<!DOCTYPE HTML>
<html>
<head>
<title>MicroProfile Live</title>
<style>
/* about css: http://bit.ly/1eMQ42U */
body {margin: 0px;padding: 0px; font: 12px Courier New;background-color:#343434; color:white;overflow:hidden;}
ul {list-style-type: none;margin: 0;padding: 0;}
li{display: inline; float:left;border:5px; position:relative;text-align:center;}
a {
    float:left;
    text-decoration:none;
    display: inline;
    text-align: center;
	padding:5px;
	padding-bottom:0px;
	padding-top:0px;
    color: #FFFFFF;
    background-color: #343434;
}
a:hover, a:active{
	background-color: #000000;
}

ul ul {
    position:absolute;
    left:0;
    top:100%;
    margin-left:-999em;
}
li:hover ul {
    margin-left:0;
    margin-right:0;
}
ul li ul{ display:block;float:none;width:100%;}
ul li ul li{ display:block;float:none;width:100%;}
li li a{ display:block;float:none;width:100%;text-align:left;}
#nav li:hover div {margin-left:0;}
.help {position:absolute;z-index:5;text-align:left;padding:2px;margin-left:-999em;background-color: #313131;width:300px;}
.helpstart {position:absolute;z-index:5;text-align:left;padding:2px;background-color: #313131;width:300px;display:none}
.root {z-index:1;position:absolute;top:0px;left:0px;}
.filterinputsearchdiv{position:fixed; background-color: #313131;display:none;}
.filterinputsearch{width:100px;}
</style>
</head>
<body style="">
<div class="filterinputsearchdiv" id="FilterInputDiv">Filter<br><input type="text" id="FilterInput" class="filterinputsearch"></div>
<div class="helpstart" id="helpwindow" style="left:20px;top:20px">
History View:<br>
Right Click + Drag : Select Region<br>
Click + Drag: Move Selection<br>
Click Frame : Center on frame<br>
<hr>
Main View:<br>
space: Freeze capturing<br>
x : Toggle View<br>
/ : Rotate connection port % 3<br>
Ctrl + Drag: Pan<br>
Click + Drag: Pan<br>
Enter: Capture selection/Next N Frames
<hr>
<table style="width:100%">
<tr>
<td width="50%" align="left"><a href='javascript:void(0)' onclick="ShowHelp(0);">Close</a></td>
</tr>
</table>
</div>
<canvas id="DetailedView" height="100%" style="background-color:#343434;margin:0px;padding:0px;"></canvas>
<script>
"use strict"

var FRAME_HISTORY_COLOR_CPU = '#ff7f27';
var FRAME_HISTORY_COLOR_GPU = '#ffffff';

var Settings = {};
var Cookie = {};

const PERCENTILE_SAMPLES = 1000;

var HistoryHeight = 100;
var CanvasDetailedView = document.getElementById('DetailedView');
var CanvasDetailedOffscreen = document.createElement('canvas');
var FilterInput = document.getElementById('FilterInput');
var FilterInputDiv = document.getElementById('FilterInputDiv');
var FilterInputDivPos = {"x":-1,"y":-1,"w":-1,"h":-1};
var FilterInputValueLast = '';

var CanvasArray0 = [];
var CanvasArray1 = [];
var Views = [];

var ViewIndex = 0;

var nWidth = CanvasDetailedView.width;
var nHeight = CanvasDetailedView.height;
var nBackColors = ['#292929', '#343434' ];
var nBackColorsDark = ['#292929', '#272727' ];
var nBackColorOffset = '#404040';
var FontHeight = 10;
var FontHeightLarge = 12;
var FontWidth = 1;
var FontAscent = 3; //Set manually
var Font = 'Bold ' + FontHeight + 'px Courier New';
var FontLarge = 'Bold ' + FontHeightLarge + 'px Courier New';
var FontFlash = 'Bold ' + 35 + 'px Courier New';
var BoxHeight = FontHeight + 2;
var MouseX = 0;
var MouseY = 0;
var MouseReleased = false;
var MouseMoveTime = new Date();

var nBarsWidth = 80;
var nOffsetBarsX = 0;
var nOffsetBarsY = 0;
var nOffsetCountersY = 0;
var nOffsetMenuTimers = 0;
var nOffsetMenuGroup = 0;
var nOffsetMenuFunctions = 0;
var nOffsetMenuModules = 0;
var nOffsetMenuPatched = 0;

var nHoverCounter = -1;

var MouseDragOff = 0;
var MouseDragDown = 1;
var MouseDragUp = 2;
var MouseDragMove = 3;
var MouseDragState = MouseDragOff;
var MouseDragTarget = 0;
var MouseDragButton = 0;
var MouseDragKeyShift = 0;
var MouseDragKeyCtrl = 0;
var MouseDragX = 0;
var MouseDragY = 0;
var MouseDragXLast = 0;
var MouseDragYLast = 0;
var MouseDragXStart = 0;
var MouseDragYStart = 0;

var MouseDragActiveXStart = 0;
var MouseDragActiveXEnd = -1;
var MouseInCaptureButton = 0;

var ToolTipCallback = null;

var DPR = 0;
var C_HUGE = 1e10;

var ActivePreset = "Default";
var ActivePresetRO = 0;
var PresetPending = 0;
var Presets = [];
var PresetsCache = {};
var ReadOnlyPresets = [];
var ReadOnlyPresetsCache = {};

Settings.SubGraphSettings = {};
Settings.ReferenceTime = 50.0;
var ReferencePresets = [5.0, 10.0, 15.0, 20, 30, 33.33, 50, 66.66,100.0,250.0,500,1000.0];
var PercentilePresets = [0.0, 1.0, 5.0, 10.0, 50.0, 75.0, 99.0];
var ReferenceTimeTweak = -1;
var PercentileTweak = -1;

Settings.TargetTime = 30;
var TargetTimeTweak = -1;


var AggregateFrames = 60;
Settings.AggregateFrames = 60;
var AggregatePresets = [0, 10,20,30,60,90,120,500];
var AggregateHistorySize = 5;
var AggregateTweak = -1;
var AggregateCurrent = 0;

var AutoCaptureCooldown = -1;
var AutoCaptureEnabled = 0;
var AutoCaptureDefaultThreshold = 66;
Settings.AutoCaptureTheshold = AutoCaptureDefaultThreshold;
Settings.AutoCaptureRepeat = 1;
var AutoCaptureThesholdPresets = [1,3,5,10,15,30,50,66,90,100,250,500,1000];
var AutoCaptureRepeatPresets = [1,2,3,4,5,6,7,8,9,10, 25, 50, 100];
var AutoCaptureTweak = -1;
var AutoCaptureRepeatTweak = -1;
var AutoCaptureSourceTweak = -1;
var AutoCaptureSourceIndex = -1;

var CaptureFramesDefault = 30;
Settings.CaptureFrames = CaptureFramesDefault;
Settings.CaptureDelay = 0;
var CaptureFramesPresets = [5,10,15,30,50,66,90,100];
var CaptureDelayPresets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var CaptureTweak = -1;
var CaptureDelayTweak = -1;

var CaptureTriggerTime = null;
var CaptureTriggerTimeType = 0;
var CaptureTriggerDelta = 0;


var ProfileData = {};
var ProfileStackTime = [];
var ProfileStackName = [];
var ProfileMode = 0;
var ProfileRedraw0 = 0;
var ProfileRedraw1 = 0;
var ProfileRedraw2 = 0;
var ProfileFps = 0;
var ProfileMs = 0;
var ProfileFpsAggr = 0;
var ProfileFpsCount = 0;
var ProfileLastTimeStamp = new Date();
var PlotfArray = new Array();

var ConnectionStr = ["\\", "|", "/", "-" ];
var ConnectionIdx = 0;
var EnabledArray = [];


var FrameData = {};
var FRAME_COUNT = 256;
var FramePending = 0;

var WSConnected = 0;
var WSIsOpen = 0;
var WSSeconds = 0;
var WSFail = 0;
var WS;
var WSHost = location.hostname ? location.hostname : "localhost";
var WSPort = location.port ? location.port : 1338;
var WSPath;

var CaptureButtonX = 0;
var CaptureButtonY = 0;
var GroupsEnabled = 0;
var TimersEnabled = 0;

var TimersActiveOnly = 0;


var MSG_TIMER_TREE = 1;
var MSG_ENABLED = 2;
var MSG_FRAME = 3;
var MSG_LOADSETTINGS = 4; 
var MSG_PRESETS = 5;
var MSG_CURRENTSETTINGS = 6; 
var MSG_COUNTERS = 7; 
var MSG_FUNCTION_RESULTS = 8;
var MSG_INACTIVE_FRAME = 9;
var MSG_FUNCTION_NAMES = 10;
var MSG_INSTRUMENT_ERROR = 11;
// var MSG_MODULE_NAME = 12;


var TYPE_NONE = 0;
var TYPE_TIMER = 1;
var TYPE_GROUP = 2;
var TYPE_CATEGORY = 3;
var TYPE_SETTING = 4;
var TYPE_COUNTER = 5;


var WSSend = 0;
var WSReceive = 0;
var WSSendBytes = 0;
var WSReceiveBytes = 0;
var WSOpenTime = 0;


var TimerArray = [];
var CounterArray = [];
var Empty = {"id":0, "w":0, "depth":0, "sibling":-1,"parent":-1,"firstchild":-1};
var WidthArray = [];
var FunctionQueryArray = [];
var FunctionQueryPending = null;
var FunctionQueryLastRequest = 0;
var FunctionQueryReceived = 0;
var WidthTree = 0;
Settings.ViewActive = 0;
Settings.ViewCompressed = 0;
Settings.AllowHighDPI = 1;
var ViewNames = ["Graph", "Graph", "Graph", "Bars", "Bars", "Bars", "Counters"];
var ViewNames2 = ["[split]", "[percentile]","[group/thread]","[table]", "[all]", "[single]", ""];

var VIEW_GRAPH_SPLIT = 0;
// var VIEW_GRAPH = 1;
var VIEW_GRAPH_PERCENTILE = 1;
var VIEW_GRAPH_THREAD_GROUP = 2;
var VIEW_BAR = 3;
var VIEW_BAR_ALL = 4;
var VIEW_BAR_SINGLE = 5;
var VIEW_COUNTERS = 6;
var VIEW_SIZE = 7;

var GRAPH_ALPHA = 0.5;


// Settings.FancyGraph = 1;
Settings.AutomaticReference = 1;
Cookie.CodeReportMode = 0; // 0: prompt, 1:always send, 2: never send, never prompt


var ReferenceHistory = 0;
var ReferenceGraph = 0;
var ReferenceBar = 0;
var ReferenceHistoryAutomatic = 0;
var ReferenceGraphAutomatic = 0;
var ReferenceGraphAutomaticGroup = 0;
var ReferenceBarAutomatic = 0;

var SingleTimerBars = 0;
var History;
var MainView;
var X7Views;
var X7LegendView;
var X7BarColumnRemap = [0,1,2,3,4,5,6];
var X7BarColumnMask = -1;
var X7LegendOffset = 25;
var X7BarLastView = -1;
var X7BarFirstView = -1;


var ViewBarMaxMsTextLength = 0;

Settings.SortColumnOrderFlip = 0;
Settings.SortColumnName = "";
var SortColumnMouseOverNext = "";


var KeyShiftDown = 0;
var KeyCtrlDown = 0;

var IsFrozen = 0;

var PresetToLoad;
var PresetToLoadRO = 0;
var HelpFade;


TimerArray.push(Empty); // 0 is root of tree

var StrTime = "Time";
var StrExclusive = "Excl Time";
var StrGroup = "Group";
var StrThread = "Thread";
var StrTimer = "Timer";
var StrAverage = "Average";
var StrMax = "Max";
var StrTotal = "Total";
var StrExclTotal = "Excl Total";
var StrMin = "Min";
var StrSpike = "Spike%";
var StrCallAverage = "Call Average";
var StrCount = "Count";
var StrExclAverage = "Excl Average";
var StrExclMax = "Excl Max";
var StrExclMin = "Excl Max";
var StrCallExclAverage = "Call Excl Avg";


var CounterNameWidth = 100;
var CounterValueWidth = 100;
var CounterLimitWidth = 100;

var FormatCounterDefault = 0;
var FormatCounterBytes = 1;
var FormatCounterBytesExt = ["b","kb","mb","gb","tb","pb","eb","zb","yb"];

var BarColumnNamesTable = [StrTime, StrExclusive, StrAverage, StrMax, StrMin, StrTotal, StrExclAverage, StrExclMax, StrExclTotal, StrSpike, StrCallAverage, StrCallExclAverage, StrCount];
var BarColumnNamesMulti = [StrTime, StrAverage, StrMax, StrMin, StrExclAverage, StrExclMax, StrExclMin];
var BarColumnNamesSingle = [StrAverage, StrMax, StrMin, StrExclAverage, StrExclMax, StrExclMin, StrCallAverage];

// var SYMBOLSTATE_DEFAULT = 0;
// var SYMBOLSTATE_LOADING = 1;
// var SYMBOLSTATE_DONE = 2;


var SymbolState;
var ModuleState = [];

var FunctionsInstrumented = [];
var FunctionsInstrumentedUnmangled = [];
var FunctionsInstrumentedModule = [];
var PopupsAllowed = 0;
var PopupsFailed = 0;
var PopupTestPending = 0;

var ThreadInfo = {};

function ConvertHslToRGB(h, s, l) //from https://gist.github.com/mjackson/5311256
{
	var r, g, b;
	if (s == 0)
	{
		r = g = b = l; // achromatic
	}
	else
	{
		function hue2rgb(p, q, t)
		{
			if (t < 0) t += 1;
			if (t > 1) t -= 1;
			if (t < 1/6) return p + (q - p) * 6 * t;
			if (t < 1/2) return q;
			if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
			return p;
		}
		var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
		var p = 2 * l - q;
		r = hue2rgb(p, q, h + 1/3);
		g = hue2rgb(p, q, h);
		b = hue2rgb(p, q, h - 1/3);
	}
	var color = ((r*255)<<16) | ((g*255)<<8) | (b*255);
	return ("000000" + color.toString(16)).slice(-6);
}


function GetBarColumnNames()
{
	if(Settings.ViewActive == VIEW_BAR_ALL)
	{
		return BarColumnNamesMulti;
	}
	else if(Settings.ViewActive == VIEW_BAR_SINGLE)
	{
		return BarColumnNamesSingle;
	}
	else
	{
		return BarColumnNamesTable;
	}
}
function GetBarColumnEnabled()
{
	if(Settings.ViewActive == VIEW_BAR_ALL)
	{
		return Settings.BarColumnEnabledMulti;
	}
	else if(Settings.ViewActive == VIEW_BAR_SINGLE)
	{
		return Settings.BarColumnEnabledSingle;
	}
	else
	{
		return Settings.BarColumnEnabledTable;
	}
}

Settings.BarColumnEnabledTable = new Array(BarColumnNamesTable.length);
Settings.BarColumnEnabledSingle = new Array(BarColumnNamesSingle.length);
Settings.BarColumnEnabledMulti = new Array(BarColumnNamesMulti.length);
var ColumnsWidth = new Array(BarColumnNamesTable.length);
function ClearEnabled(E)
{
	for(var i = 0; i < E.length; ++i)
	{
		E[i] = 1;
		ColumnsWidth[i] = 10;
	}
}
ClearEnabled(Settings.BarColumnEnabledTable);
ClearEnabled(Settings.BarColumnEnabledSingle);
ClearEnabled(Settings.BarColumnEnabledMulti);

function Plotf(str)
{
	PlotfArray.push(str);
}
function PlotfClear()
{
	PlotfArray = new Array();
}

function ProfileModeClear()
{
	if(ProfileMode)
	{
		for(var idx in ProfileData)
		{
			if(idx == "Plot")
				continue;
			var Timer = ProfileData[idx];
			Timer.Count = 0;
			Timer.Time = 0;
		}

		// ProfileData = new Object();
		ProfileStackTime = new Array();
		ProfileStackName = new Array();
	}
}
function ProfileEnter(Name)
{
	if(ProfileMode)
	{
		ProfileStackTime.push(performance.now());
		ProfileStackName.push(Name);
	}
}
function ProfileLeave()
{
	if(ProfileMode)
	{
		var Time = performance.now();
		var Delta = Time - ProfileStackTime.pop();
		var Name = ProfileStackName.pop();
		var Obj = ProfileData[Name];
		if(!Obj)
		{
			Obj = new Object();
			Obj.Count = 0;
			Obj.Name = Name;
			Obj.Time = 0;
			Obj.AggrCount = 0;
			Obj.AggrTime = 0;
			Obj.AggrMax = 0;
			Obj.AvgTime = 0;
			Obj.MaxTime = 0;
			Obj.TotalTime = 0;
			ProfileData[Name] = Obj;
		}
		Obj.Time += Delta;
		Obj.Count += 1;
	}
}

function ProfilePlot(s)
{
	if(ProfileMode)
	{
		var A = ProfileData.Plot;
		if(!A)
		{
			ProfileData.Plot = Array();
			A = ProfileData.Plot;
		}
		if(A.length<10)
		{
			A.push(s);
		}
	}
}
function ProfileModeDump()
{
	for(var idx in ProfileData)
	{
		var Timer = ProfileData[idx];
		console.log(Timer.Name + " " + Timer.Time + "ms " + Timer.Count);
	}

}
function ProfileModeDraw(Canvas)
{
	if(ProfileMode)
	{
		ProfileFpsCount ++ ;
		var AggrFrames = 60;
		var StringArray = [];
		function FormatTime(f)
		{
			return ("             " + f.toFixed(2)).slice(-12);
		}
		function FormatStr(t, count, avg, max, total)
		{
			var str = FormatTime(t) + "ms " + ("        #" + count).slice(-8) + 
			"" + FormatTime(avg) + "ms " + FormatTime(max) + "ms " + FormatTime(total) + "ms";
			return str;
		}
		StringArray.push("");
		StringArray.push("time    count         avg            max       total/" +  AggrFrames + "  ");

		for(var idx in ProfileData)
		{
			if(idx == "Plot")
				continue;
			var Timer = ProfileData[idx];
			Timer.AggrCount += Timer.Count;
			Timer.AggrTime += Timer.Time;
			Timer.AggrMax = Math.max(Timer.AggrMax, Timer.Time);
			if(ProfileFpsCount == AggrFrames)
			{
				Timer.AvgTime = Timer.AggrTime / AggrFrames;
				Timer.MaxTime = Timer.AggrMax;
				Timer.TotalTime = Timer.AggrTime;
				Timer.AggrCount = 0;
				Timer.AggrTime = 0;
				Timer.AggrMax = 0;
			}
			StringArray.push(Timer.Name);
			StringArray.push(FormatStr(Timer.Time, Timer.Count, Timer.AvgTime, Timer.MaxTime, Timer.TotalTime));
		}
		var Time = new Date();
		var Delta = Time - ProfileLastTimeStamp;
		ProfileLastTimeStamp = Time;
		StringArray.push("Frame Delta");
		StringArray.push(Delta + "ms");
		{
			ProfileFpsAggr += Delta;

			if(ProfileFpsCount == AggrFrames)
			{
				ProfileMs = ProfileFpsAggr / AggrFrames;
				ProfileFps = 1000 / (ProfileFpsAggr / AggrFrames);
				ProfileFpsAggr = 0;
				ProfileFpsCount = 0;
			}
			StringArray.push("Avg FPS");
			StringArray.push("" + ProfileFps.toFixed(2));
			StringArray.push("Avg MS");
			StringArray.push("" + ProfileMs.toFixed(2));
		}
		for(var i = 0; i < ProfileData.Plot; ++i)
		{
			StringArray.push("");
			StringArray.push(ProfileData.Plot[i]);
		}
		ProfileData.Plot = Array();
		DrawToolTip(StringArray, Canvas, 0, 200);
	}
}



function MeasureFont()
{
	var context = CanvasDetailedView.getContext('2d');
	context.font = Font;
	FontWidth = context.measureText('W').width;

}
function ResizeCanvasDPR(w, h, c)
{
	DPR = Settings.AllowHighDPI ? window.devicePixelRatio : 0;
	if(DPR)
	{
		c.style.width = w + 'px'; 
		c.style.height = h + 'px';
		c.width = w * DPR;
		c.height = h * DPR;
		c.getContext('2d').scale(DPR,DPR);
	}
	else
	{
		DPR = 1;
		c.width = w;
		c.height = h;
	}

}

function ResizeCanvasDPR2(w, h, c)
{
	DPR = window.devicePixelRatio;
	if(DPR)
	{
		c.style.width = w + 'px'; 
		c.style.height = h + 'px';
		c.width = w * DPR;
		c.height = h * DPR;
		c.getContext('2d').scale(DPR,DPR);
	}
	else
	{
		c.width = w;
		c.height = h;
	}

}
function ResizeView(View, x, y, w, h)
{
	View.x = x;
	View.y = y;
	View.w = w;
	View.h = h;
	var c0 = View.Canvas[0];
	var c1 = View.Canvas[1];
	ResizeCanvasDPR(w, h, c0);
	ResizeCanvasDPR(w, h, c1);
	c0.getContext('2d').clearRect(0, 0, w, h);
	c1.getContext('2d').clearRect(0, 0, w, h);
	View.OffscreenData[0] = c0.getContext('2d').getImageData(0, 0, c0.width, c0.height);
	View.OffscreenData[1] = c1.getContext('2d').getImageData(0, 0, c1.width, c1.height);

}
function CreateView(x, y, w, h, name, DisplayFunc, visible, index)
{
	var idx = Views.length;
	var c0 = CanvasArray0[idx];
	var c1 = CanvasArray1[idx];
	if(!c0)
	{
		c0 = document.createElement('canvas');
		CanvasArray0[idx] = c0;
	}
	if(!c1)
	{
		c1 = document.createElement('canvas');
		CanvasArray1[idx] = c1;
	}
	var View = {};
	View.x = x;
	View.y = y;
	View.w = w;
	View.h = h;
	View.Canvas = [c0, c1];
	View.OffscreenData = [null, null];
	View.visible = visible;
	View.index = index;
	ResizeCanvasDPR(w, h, c0);
	ResizeCanvasDPR(w, h, c1);

	c0.getContext('2d').clearRect(0, 0, w, h);
	c1.getContext('2d').clearRect(0, 0, w, h);
	View.OffscreenData[0] = c0.getContext('2d').getImageData(0, 0, c0.width, c0.height);
	View.OffscreenData[1] = c1.getContext('2d').getImageData(0, 0, c1.width, c1.height);
	View.BackBuffer = 0;
	View.DisplayFunc = DisplayFunc;
	Views.push(View);
	return View;
}

function CreateViews(Width, Height, ViewCompressed)
{
	Views = [];
	var HistoryH = ViewCompressed ? 0 : HistoryHeight;
	History = CreateView(0, 0, Width, HistoryHeight, "History", DrawHistory, true, 0);
	History.visible = !ViewCompressed;
	MainView = CreateView(0, HistoryH, Width, Height-HistoryH, "Main", DrawGraphSplit, true);
	X7Views = [];
	var w = Width / 7;
	var x = 0;
	X7Views.push(CreateView(w*0, HistoryH, w, Height - HistoryH, "x5_0", DrawBars, false, 0) );
	X7Views.push(CreateView(w*1, HistoryH, w, Height - HistoryH, "x5_1", DrawBars, false, 1) );
	X7Views.push(CreateView(w*2, HistoryH, w, Height - HistoryH, "x5_2", DrawBars, false, 2) );
	X7Views.push(CreateView(w*3, HistoryH, w, Height - HistoryH, "x5_3", DrawBars, false, 3) );
	X7Views.push(CreateView(w*4, HistoryH, w, Height - HistoryH, "x5_4", DrawBars, false, 4) );
	X7Views.push(CreateView(w*5, HistoryH, w, Height - HistoryH, "x5_3", DrawBars, false, 5) );
	X7Views.push(CreateView(w*6, HistoryH, w, Height - HistoryH, "x5_3", DrawBars, false, 6) );
	X7LegendView = CreateView(0, Height-X7LegendOffset, Width, X7LegendOffset, "x7_legend", DrawBarsLegend, false, 0);

}

function ResizeCanvas() 
{
	nWidth = window.innerWidth;
	nHeight = window.innerHeight;
	DPR = Settings.AllowHighDPI ? window.devicePixelRatio : 0;
	ResizeCanvasDPR(nWidth, nHeight, CanvasDetailedView);
	ResizeCanvasDPR(nWidth, nHeight, CanvasDetailedOffscreen);

	if(DPR)
	{
		CanvasDetailedView.style.width = nWidth + 'px'; 
		CanvasDetailedView.style.height = nHeight + 'px';
		CanvasDetailedView.width = nWidth * DPR;
		CanvasDetailedView.height = nHeight * DPR;
		CanvasDetailedView.getContext('2d').scale(DPR,DPR);

		CanvasDetailedOffscreen.style.width = nWidth + 'px';
		CanvasDetailedOffscreen.style.height = nHeight + 'px';
		CanvasDetailedOffscreen.width = nWidth * DPR;
		CanvasDetailedOffscreen.height = nHeight * DPR;
		CanvasDetailedOffscreen.getContext('2d').scale(DPR,DPR);

	}
	else
	{
		DPR = 1;
		CanvasDetailedView.width = nWidth;
		CanvasDetailedView.height = nHeight;
		CanvasDetailedOffscreen.width = nWidth;
		CanvasDetailedOffscreen.height = nHeight;
	}
	MeasureFont();
	CreateViews(nWidth, nHeight, Settings.ViewCompressed);
	ActivateView(Settings.ViewActive);
}


function FormatTime(Time)
{
	return Time.toFixed(2);
}
var hh = 0;


function DrawBarsLegend(View, LocalMouseX, LocalMouseY, SubIndex)
{
	ProfileEnter("DrawBar");
	var TimerMap = FrameData.TimerMap;
	if(!TimerMap)
		return;
	if(Settings.ViewCompressed)
		return;
	var Canvas = View.Canvas[View.BackBuffer];
	var context = Canvas.getContext('2d');
	context.clearRect(0, 0, View.w, View.h);
	var X = 0;
	var Y = View.h/2;
	var XSpace = 5;
	var XSpace2 = XSpace * 2;
	function DrawEntry(T)
	{
		X += XSpace2*2;
		context.fillStyle = T.color;
		context.fillRect(X-XSpace,Y-XSpace,XSpace2,XSpace2);
		X += XSpace + 2;
		context.fillStyle = 'white';
		var w = context.measureText(T.name).width;
		context.fillText(T.name, X, Y + FontHeight/2);
		X += w;		
	}

	if(SingleTimerBars == 0)
	{
		for(var key in TimerMap)
		{
			var idx = GetTimer(key);
			var T = TimerArray[idx];
			if(T.e)
			{
				DrawEntry(T);
			}
		}
	}
	else if(EnabledArray.length > 0)
	{
		var idx = EnabledArray[0];
		var T = TimerArray[idx];
		DrawEntry(T);
	}		
}

function DrawBars(View, LocalMouseX, LocalMouseY, SubIndex)
{
	var TimerMap = FrameData.TimerMap;
	if(!TimerMap)
		return;
	if(!SubIndex)
		SubIndex = 0;

	ProfileEnter("DrawBar");
	var Canvas = View.Canvas[View.BackBuffer];
	var context = Canvas.getContext('2d');

	context.clearRect(0, 0, View.w, View.h);
	var bgcolor = nBackColors[ViewIndex%2];
	context.fillStyle = bgcolor;
	context.fillRect(0, 0, View.w, View.h);
	var Title = "?";
	var TitleName = null;
	var nNumBars = 0;
	var BarNames = [];
	var BarTimes = [];
	var BarColors = [];
	var AggregateIndex = Settings.AggregateFrames <= 0 ? AggregateHistorySize-1 : AggregateHistorySize-2; //fix med 
	var GetTime = null;
	var SubIndex = X7BarColumnRemap[SubIndex];

	if(SingleTimerBars == 0)
	{
		if(SubIndex == 0)
		{
			Title = "Time";
			GetTime = function(FD){ return FD.FrameTime; };
		}
		else if(SubIndex == 1)
		{
			Title = "Average";
			GetTime = function(FD){ return FD.TimeAvg[AggregateIndex]; };
		}
		else if(SubIndex == 2)
		{
			Title = "Max";
			GetTime = function(FD){ return FD.TimeMax[AggregateIndex]; };
		}
		else if(SubIndex == 3)
		{
			Title = "Min";
			GetTime = function(FD){ return FD.TimeMin[AggregateIndex]; };
		}
		else if(SubIndex == 4)
		{
			Title = "Exclusive Avg";
			GetTime = function(FD){ return FD.TimeExclAvg[AggregateIndex]; };
		}
		else if(SubIndex == 5)
		{
			Title = "Exclusive Max";
			GetTime = function(FD){ return FD.TimeExclMax[AggregateIndex]; };
		}
		else if(SubIndex == 6)
		{
			Title = "Exclusive Min";
			GetTime = function(FD){ return FD.TimeExclMin[AggregateIndex]; };
		}
		for(var key in TimerMap)
		{
			var idx = GetTimer(key);
			var T = TimerArray[idx];
			if(T.e)
			{
				nNumBars++;
				var FD = TimerMap[key];
				var Time = GetTime(FD);
				BarNames.push(T.name);
				BarTimes.push(Time);
				BarColors.push(T.color);
			}
		}

	}
	else if(EnabledArray.length > 0)
	{
		var idx = EnabledArray[0];
		var T = TimerArray[idx];
		var FD =  GetFrameData(T.id);
		var Property = null;
		if(SubIndex == 0)
		{
			Title = "Average";
			Property = "TimeAvg";
		}
		else if(SubIndex == 1)
		{
			Title = "Max";
			Property = "TimeMax";
		}
		else if(SubIndex == 2)
		{
			Title = "Min";
			Property = "TimeMin";
		}
		else if(SubIndex == 3)
		{
			Title = "Exclusive Average";
			Property = "TimeExclAvg";
		}
		else if(SubIndex == 4)
		{
			Title = "Exclusive Max";
			Property = "TimeExclMax";
		}
		else if(SubIndex == 5)
		{
			Title = "Exclusive Min";
			Property = "TimeExclMin";
		}
		else if(SubIndex == 6)
		{
			Title = "Call Average";
			Property = "TimeCallAvg";
		}
		else if(SubIndex == 7)
		{
			Title = "Call Excl Average";
			Property = "TimeCallExclAvg";
		}
		TitleName = T.name;
		for(var i = 0; i < AggregateHistorySize; ++i)
		{
			nNumBars++;
			var A = FD[Property];
			var Time = A[i];
			BarTimes.push(Time);
			BarColors.push(T.color);
		}
	}


	if(!nNumBars)
	{
		ProfileLeave();
		return;
	}

	var h = View.h;
	var w = View.w;
	var MsTextExtraSpace = Math.cos(3.14/4.0) * (ViewBarMaxMsTextLength);
	var DrawXLeft = Settings.ViewCompressed ? 3 : 15;
	DrawXLeft = Math.max(DrawXLeft, MsTextExtraSpace);
	var DrawXRight = Settings.ViewCompressed ? 3: 10;
	var DrawY = 35 * 2;
	if(Settings.ViewCompressed)
	{
		DrawY = (MsTextExtraSpace) + 35;
	}
	var DrawWidth = w - DrawXLeft - DrawXRight;
	var DrawHeight = h - DrawY;
	var SpaceWidth = 5;
	var BarWidth = (DrawWidth-SpaceWidth*(nNumBars-1))/ nNumBars;
	for(var x = 0; x < 2; ++x)
	{
		if(BarWidth < 14)
		{
			SpaceWidth -= 1;
			BarWidth = (DrawWidth-SpaceWidth*(nNumBars-1))/ nNumBars;
		}
	}
	if(BarWidth > 50)
		BarWidth = 50;
	var BarHeight = DrawHeight - 5;

	var ReferenceTime = ReferenceBar;

	var fHeightScale = h / ReferenceTime;
	var MouseDragging = 0;
	var fWidth = w / FRAME_COUNT;
	var Keys = [];
	var X = DrawXLeft;
	var offset = 0;
	context.textAlign = 'center';
	context.fillStyle = '#ffffff';
	context.fillText(Title, w / 2.0, FontHeight);
	context.textAlign = 'left';
	context.fillStyle = 'wheat';
	var BaseY = 20;
	if(SubIndex == X7BarFirstView)
	{
		context.fillText(ReferenceTime.toFixed(2) + 'ms', 0, BaseY - 5 + DrawHeight - BarHeight);
	}
	else if(SubIndex == X7BarLastView)
	{
		context.textAlign = 'right';
		context.fillText(ReferenceTime.toFixed(2) + 'ms', w, BaseY - 5 + DrawHeight - BarHeight);
	}
	context.textAlign = 'right';
	var BarFont = FontLarge;
	var DrawNames = true;
	if(BarWidth < 14)
	{
		DrawNames = BarWidth > 4;
		var FontXX = 'Bold ' + Math.floor(BarWidth) + 'px Courier New';
		BarFont = FontXX;

	}
	context.font = BarFont;
	for(var i = 0; i < BarTimes.length; ++i)
	{
		var TimeText = FormatTime(Time);
		var w = context.measureText(TimeText).width;
		ViewBarMaxMsTextLength = Math.max(w, ViewBarMaxMsTextLength);
	

	}
	for(var i = 0; i < BarTimes.length; ++i)
	{
		var Time = BarTimes[i];
		var TimeText = FormatTime(Time);
		ReferenceBarAutomatic = Math.max(Time, ReferenceBarAutomatic);
		var Color = BarColors[i];
		var fPrc = Time / ReferenceTime;
		if(fPrc > 1.0)
			fPrc = 1.0;
		var BarH = fPrc * BarHeight;

		var X0 = X;
		var Y0 = BaseY + DrawHeight - BarH;
		context.fillStyle = Color;
		context.fillRect(X0, Y0, BarWidth, BarH);
		context.fillStyle = '#ffffff';
		var MouseOver = LocalMouseX > X0 && LocalMouseX < X0 + BarWidth;
		if(MouseOver || (Settings.ViewCompressed&&DrawNames))
		{
			context.save();
			context.translate(X0 + BarWidth * 0.5, BaseY + DrawHeight - 2);
			context.rotate(-3.14/2.0);
			context.font = BarFont;
			context.textAlign = 'left';
			context.textBaseline = 'middle';
			var m = context.measureText(BarNames[i]);
			context.fillStyle = 'black';
			context.fillText(BarNames[i], -1, -1);
			context.fillStyle = 'white';
			context.fillText(BarNames[i], 0, 0);
			context.textAlign = 'right';
			context.restore();
		}
		context.save();
		var XText = X+BarWidth;
		var YText = BaseY + DrawHeight + FontHeight;		
		context.translate(XText, YText);
		context.rotate(-3.14/4.0);
		context.font = BarFont;
		context.fillText(TimeText, 0, 0);
		context.restore();

		X += BarWidth + SpaceWidth;
	}
	context.font = Font;
	ProfileLeave();
}

function SortColumnFromName(Name)
{
	//should match switch in drawtableview
	if(Name == StrAverage)
	{
		return 1;
	}
	else if(Name == StrMax)
	{
		return 2;
	}
	else if(Name == StrTotal)
	{
		return 3;
	}			
	else if(Name == StrMin)
	{
		return 4;
	}
	else if(Name == StrSpike)
	{
		return 5;
	}
	else if(Name == StrCallAverage)
	{
		return 6;
	}
	else if(Name == StrCount)
	{
		return 7;
	}
	else if(Name == StrExclAverage)
	{
		return 8;
	}
	else if(Name == StrExclMax)
	{
		return 9;
	}
	else if(Name == StrGroup)
	{
		return -1;
	}
	else if(Name == StrTimer)
	{
		return -2;
	}
	return 0;
}

function DrawTableView(View, LocalMouseX, LocalMouseY, SubIndex)
{
	ProfileEnter("DrawTableView");
	var Canvas = View.Canvas[View.BackBuffer];
	var context = Canvas.getContext('2d');
	var Height = BoxHeight;
	var Width = nWidth;
	var Y = Height;
	var XBase = 0;
	var nColorIndex = 0;
	var bMouseIn = 0;
	var RcpReferenceTime = 1.0 / Settings.ReferenceTime;
	var CountWidth = 12 * FontWidth;
	var InnerBoxHeight = BoxHeight-2;
	var TimerLen = 8;
	var TimerWidth = TimerLen * FontWidth;
	var nWidthBars = nBarsWidth+2;
	var nWidthMs = TimerWidth+2+10;
	var NameWidth = 200;
	var R = 0;

	var OffsetY = BoxHeight;


	context.clearRect(0, 0, View.w, View.h);
	context.fillStyle = 'white';
	context.font = Font;


	function HeaderMouseHandle(XBegin, X, Y, Header)
	{
		if(Header == null)
			debugger;
		var bMouseIn = LocalMouseY >= Y && LocalMouseY < BoxHeight+Y && LocalMouseX < X && LocalMouseX > XBegin;
		if(bMouseIn)
		{
			SortColumnMouseOverNext = Header;
		}
	}
	function HeaderString(Header)
	{
		if(Header == Settings.SortColumnName)
		{
			return Header + (Settings.SortColumnOrderFlip ? '<' : '>');
		}
		else
		{
			return Header;
		}

	}
	function DrawHeaderSplit(Header, Y)
	{
		if(Settings.BarColumnEnabledTable[R])
		{
			context.fillStyle = 'white';
			context.fillText(HeaderString(Header), X, Y + Height-FontAscent);
			var XBegin = X;
			X += nWidthBars;
			context.fillStyle = nBackColorOffset;
			X += ColumnsWidth[R];

			if(X >= NameWidth)
			{
				context.fillRect(X-3, Y, 1, nHeight);
			}
			HeaderMouseHandle(XBegin, X, Y, Header);
		}
		R++;
	}
	function DrawHeaderSplitSingle(Header, Y)
	{
		if(Settings.BarColumnEnabledTable[R])
		{
			context.fillStyle = 'white';
			context.fillText(HeaderString(Header), X, Y + Height-FontAscent);
			var XBegin = X;
			X += ColumnsWidth[R];
			context.fillStyle = nBackColorOffset;
			if(X >= NameWidth)
			{
				context.fillRect(X-3, Y, 1, nHeight);
			}
			HeaderMouseHandle(XBegin, X, Y, Header);
		}
		R++;
	}
	function DrawHeaderSplitLeftRight(HeaderLeft, HeaderRight, Y, Width)
	{
		var HeaderLeftS = HeaderString(HeaderLeft);
		var HeaderRightS = HeaderString(HeaderRight);
		context.textAlign = 'left';
		context.fillStyle = 'white';
		context.fillText(HeaderLeftS, X, Y + Height-FontAscent);
		var wLeft = context.measureText(HeaderLeftS).width;
		var XBegin = X;
		X += Width;
		context.textAlign = 'right';
		context.fillText(HeaderRightS, X-5, Y + Height-FontAscent);
		context.textAlign = 'left';
		context.fillStyle = nBackColorOffset;
		if(X >= NameWidth)
		{
			context.fillRect(X-3, 0, 1, nHeight);
		}
		HeaderMouseHandle(XBegin, XBegin + wLeft, Y, HeaderLeft);
		HeaderMouseHandle(XBegin + wLeft, X, Y, HeaderRight);

	}
	function DrawTimer(Value, Color)
	{
		if(Settings.BarColumnEnabledTable[R])
		{
			if(null == Value)
			{
				X += nWidthBars + ColumnsWidth[R];
				console.log("Should not happen2\n");
				debugger;
				return;
			}
			var Prc = Value * RcpReferenceTime;
			var YText = Y+Height-FontAscent;
			if(Prc > 1)
			{
				Prc = 1;
			}
			context.textAlign = 'left';
			context.fillStyle = Color;
			context.fillRect(X+1, Y+1, Prc * nBarsWidth, InnerBoxHeight);
			X += nWidthBars;
			context.fillStyle = 'white';
			var TimerText = Value.toFixed(2);
			var W = context.measureText(TimerText).width + FontWidth;
			ColumnsWidth[R] = Math.max(W, ColumnsWidth[R]);
			X += ColumnsWidth[R];
			context.textAlign = 'right';
			context.fillText(TimerText, X - FontWidth, YText);
			context.textAlign = 'left';
		}
		R++;
	}
	function DrawCount(Str)
	{
		if(Settings.BarColumnEnabledTable[R])
		{
			X += ColumnsWidth[R];
			context.fillStyle = 'white';
			context.textAlign = 'right';
			var YText = Y+Height-FontAscent;
			context.fillText(Str, X-6, YText);
			var W = Math.max(80, context.measureText(Str).width + FontWidth * 2);
			ColumnsWidth[R] = Math.max(W, ColumnsWidth[R]);

		}
		R++;
	
	}
	function DrawMeta(Value, Width, Dec, YText)
	{
		Value = FormatMeta(Value, Dec);
		X += (FontWidth*Width);
		context.textAlign = 'right';
		context.fillText(Value, X-FontWidth, YText);
		context.textAlign = 'left';
	}

	function DrawTimerRow(idx, showgroup)
	{
		R = 0;
		var T = TimerArray[idx];
		var key = T.id;
		var FD = TimerMap[key];
		var AggregateIndex = Settings.AggregateFrames <= 0 ? AggregateHistorySize-1 : AggregateHistorySize-2;
	
		var YText = Y+Height-FontAscent;
		X = NameWidth + XBase;

		nColorIndex = 1-nColorIndex;
		bMouseIn = LocalMouseY >= Y && LocalMouseY < Y + BoxHeight;
		context.fillStyle = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
		context.fillRect(0, Y, Width, FontHeight+2);

		DrawTimer(T.time, T.color);
		DrawTimer(T.excl, T.color);

		DrawTimer(T.average, T.color);
		DrawTimer(T.max, T.color);
		DrawTimer(T.min, T.color);
		DrawTimer(T.total, T.color);

		DrawTimer(T.exclaverage, T.color);
		DrawTimer(T.exclmax, T.color);
		DrawTimer(T.excltotal, T.color);

		DrawCount((T.spike?T.spike.toFixed(2):"0") + '%');
		DrawTimer(T.callaverage, T.color);
		DrawTimer(T.callexclaverage, T.color);
		DrawCount('' + T.callcount);

		context.fillStyle = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
		context.fillRect(0, Y, NameWidth, Height);
		if(T.idtype == TYPE_GROUP)
		{
			context.textAlign = 'left';
			context.fillStyle = T.color;
			context.fillText(T.name, 1, YText);
		}
		else
		{
			context.textAlign = 'right';
			context.fillStyle = T.color;
			context.fillText(T.name, NameWidth - 5, YText);
			context.textAlign = 'left';
			let P = TimerArray[T.parent];
			context.fillStyle = P.color;
			var ParentName = P.name;
			context.fillText(ParentName, 1, YText);
		}
	}
	function FilterMatch(FilterArray, value)
	{
		if(!FilterArray)
			return true;
		for(var i = 0; i < FilterArray.length; ++i)
		{
			var res = value.search(FilterArray[i]);
			if(res<0)
				return false;
		}
		return true;
	}
	var TimerMap = FrameData.TimerMap;
	if(!TimerMap)
	{
		return;
	}

	var wfirst = 100;
	var OrderArray = new Array();
	var nTotalRows = 0;
	for(var key in TimerMap)
	{
		var idx = GetTimer(key);
		var T = TimerArray[idx];
		if(T.e)
		{
			OrderArray.push(idx);
			wfirst = wfirst < T.wtotal ? T.wtotal : wfirst;
			nTotalRows++;
		}
	}
	NameWidth = wfirst + 20;


	var nTotalRowPixels = nTotalRows * Height;
	var nFrameRows = nHeight - HistoryHeight - BoxHeight;
	if(nTotalRowPixels > nFrameRows)
	{
		if(nOffsetBarsY + nFrameRows > nTotalRowPixels)
		{
			nOffsetBarsY = nTotalRowPixels - nFrameRows;
		}
	}
	else
	{
		nOffsetBarsY = 0;
	}
	Y = Y - nOffsetBarsY;
	Y += OffsetY;
	XBase = XBase - nOffsetBarsX;

	if(1)
	{
		let Flip = Settings.SortColumnOrderFlip == 1 ? -1 : 1;
		let StringCompare = function(Key)
		{
			let F = function(A, B)
			{
				let s1 = Key(A);
				let s2 = Key(B);
				return Flip * s2.localeCompare(s1);
			};
			return F;
		};
		let NumberCompare = function(Key)
		{
			let F = function(A, B)
			{
				let v0 = Key(B);
				let v1 = Key(A);
				return Flip * (v0 - v1);
					//Key(B) - Key(A));
			};
			return F;
		};
		let N = Settings.SortColumnName;
		if(N == StrTime)
		{
			let Do = function()
			{
				for(var i = 0; i < OrderArray.length; ++i)
				{
					let t = TimerArray[OrderArray[i]];
					console.log("", i, OrderArray[i], t.name, t.id, t.time);
				};

			};
			// Do();
			OrderArray.sort( NumberCompare( function (a) { 
				return TimerArray[a].time; 
			} ) );
			// Do();
		}
		else if(N == StrExclusive)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].excl; } ) );
		}
		else if(N == StrAverage)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].average; } ) );
		}
		else if(N == StrMax)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].max; } ) );
		}
		else if(N == StrTotal)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].total; } ) );
		}			
		else if(N == StrMin)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].min; } ) );
		}
		else if(N == StrSpike)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].spike; } ) );
		}
		else if(N == StrCallAverage)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].callaverage; } ) );
		}
		else if(N == StrCount)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].count; } ) );
		}
		else if(N == StrExclAverage)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].exclaverage; } ) );
		}
		else if(N == StrExclMax)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].exclmax; } ) );
		}
		else if(N == StrExclTotal)
		{
			OrderArray.sort( NumberCompare( function (a) { return TimerArray[a].excltotal; } ) );
		}
		else if(N == StrGroup)
		{
			OrderArray.sort( StringCompare( function (a) { return TimerArray[TimerArray[a].parent].name; } ) );
		}
		else if(N == StrTimer)
		{
			OrderArray.sort( StringCompare( function (a) { return TimerArray[a].name; } ) );
		}
		else
		{
			if(N != "")
			{
				console.log("unhandle sortkey", N);
				debugger;
			}

			OrderArray.sort( StringCompare( function (a) { return TimerArray[a].sortkey; } ) );
		}
	}

	let ColorHigh = '#85929e';


	for(var i = 0; i < OrderArray.length; ++i)
	{
		var idx = OrderArray[i];
		var T = TimerArray[idx];
		if(T.idtype == TYPE_GROUP)
		{
			DrawTimerRow(idx, 1);
			Y += Height;
		}
	}
	let SplitY = Y;

	for(var i = 0; i < OrderArray.length; ++i)
	{
		var idx = OrderArray[i];
		var T = TimerArray[idx];
		if(T.idtype != TYPE_GROUP)
		{
			DrawTimerRow(idx, 1);
			Y += Height;
		}
	}

	var X = 0;
	context.fillStyle = nBackColorOffset;
	context.fillRect(0, 0, Width, 2*Height);
	context.fillStyle = 'white';
	SortColumnMouseOverNext = null;
	X = NameWidth + XBase;
	R = 0;

	let Aggr = Settings.AggregateFrames <= 0 ? AggregateCurrent : Settings.AggregateFrames;
	let Headers = ["Per Frame", "Aggregate/" + Aggr + " Frames", "Call/" + Aggr + " Frames"];
	let SplitX = [0,0,0];

	DrawHeaderSplit(StrTime,OffsetY);
	DrawHeaderSplit(StrExclusive, OffsetY);
	SplitX[0] = X;

	DrawHeaderSplit(StrAverage, OffsetY);
	DrawHeaderSplit(StrMax, OffsetY);
	DrawHeaderSplit(StrMin, OffsetY);
	DrawHeaderSplit(StrTotal, OffsetY);

	DrawHeaderSplit(StrExclAverage, OffsetY);
	DrawHeaderSplit(StrExclMax, OffsetY);
	DrawHeaderSplit(StrExclTotal, OffsetY);
	
	DrawHeaderSplitSingle(StrSpike, OffsetY);
	SplitX[1] = X;

	DrawHeaderSplit(StrCallAverage, OffsetY);
	DrawHeaderSplit(StrCallExclAverage, OffsetY);
	DrawHeaderSplitSingle(StrCount, OffsetY);
	SplitX[2] = X;

	context.fillStyle = 'white';
	for(var i = 0; i < SplitX.length; ++i)
	{
		var X0 = i == 0 ? (NameWidth + XBase) : SplitX[i-1];
		var X1 = SplitX[i];
		if(X0 != X1)
		{
			context.fillText(Headers[i], X0 + 1, Height-FontAscent);
		}
	}


	X = 0;
	context.fillStyle = nBackColorOffset;
	context.fillRect(0, 0, NameWidth, Height * 2);
	context.fillStyle = 'white';
	DrawHeaderSplitLeftRight(StrGroup, StrTimer, 0, NameWidth);


	for(var i = 0; i < SplitX.length; ++i)
	{
		var X0 = i == 0 ? (NameWidth + XBase) : SplitX[i-1];
		var X1 = SplitX[i];
		if(X0 != X1)
		{
			context.fillStyle = ColorHigh;
			if(X1 >= NameWidth)
				context.fillRect(X1-2.5, 0, 2, nHeight);
			if(X0 >= NameWidth)
				context.fillRect(X0-2.5, 0, 2, nHeight);
		}
	}
	context.fillStyle = ColorHigh;
	context.fillRect(0, 2*Height-1, nWidth, 2);

	context.fillStyle = ColorHigh;
	context.fillRect(0, SplitY-1, nWidth, 2);


	ProfileLeave();
}
function DrawGraphThreadGroup(View, LocalMouseX, LocalMouseY, SubIndex)
{
	DrawGraphThreadExclusive(View, LocalMouseX, LocalMouseY, SubIndex);
}
function DrawGraphSplit(View, LocalMouseX, LocalMouseY, SubIndex)
{
	DrawGraph(View, LocalMouseX, LocalMouseY, SubIndex, 1);
}

function DrawGraphThreadExclusive(View, LocalMouseX, LocalMouseY, SubIndex, Split)
{
	var TimerMap = FrameData.TimerMap;
	if(!TimerMap)
		return;

	ProfileEnter("DrawGraphThreadExclusive");
	var Canvas = View.Canvas[View.BackBuffer];
	var context = Canvas.getContext('2d');
	context.clearRect(0, 0, View.w, View.h);

	var h = View.h;
	var w = View.w;
	var fHeightScale = h / ReferenceGraph;
	var MouseDragging = 0;
	var fWidth = w / FRAME_COUNT;
	var MouseTime = ReferenceGraph * (1-(LocalMouseY / h));
	var HighlightKey = 0;
	var HighlightFrame = -1;


	var NumGraphs = 0;
	for(let k in ThreadInfo)
	{
		NumGraphs++;
	}
	if(NumGraphs)
	{
		let hstart = 0;
		let gh = h / NumGraphs;
		let cidx = 1;
		let Count = FRAME_COUNT;
		let Last = AllocClearedArray(Count);
		let FT = FrameData.Time;
		let RcpFT = AllocClearedArray(Count);
		if(FT.length != RcpFT.length)
			debugger;
		for(let k = 0; k < RcpFT.length; ++k)
		{
			if(FT[k] != 0)
			{
				RcpFT[k] = 1.0 / FT[k];
			}
			else
			{
				RcpFT[k] = 0;
			}
		}

		for(let k in ThreadInfo)
		{
			for(let j = 0; j < Last.length; ++j)
				Last[j] = 0.0;
			let TI = ThreadInfo[k];
			let X = 0;
			let Y = hstart + gh;
			var YStart = Y;
			if(LocalMouseX >= 0 && LocalMouseY >= hstart && LocalMouseX < w && LocalMouseY <= Y)
			{
				HighlightKey = k;
				HighlightFrame = Math.floor(FRAME_COUNT * LocalMouseX / w);	
			}


			context.globalAlpha = 1;
			context.fillStyle = nBackColorsDark[cidx];
			cidx = 1-cidx;
			context.fillRect(0, hstart, w, gh);
			context.strokeStyle = 'white';
			context.fillStyle = 'white';
			for(let l = 0; l < TI.a.length; ++l)
			{
				X = 0;
				let a = TI.a[l];
				if(a.length != Count)
				{
					console.log("should not happen!\n");
					debugger;
				}
				let idx = GetTimer(TI.ids[l]);
				let c = TimerArray[idx].color;
				context.strokeStyle = c;
				context.fillStyle = c;
				context.beginPath();

				for(let m = Last.length-1;m >= 0; m--)
				{
					let XX = X + fWidth * m;
					Y = Math.max(YStart - Last[m] * gh, hstart);
					if(m == Last.length-1)
					{
						context.moveTo(XX, Y);
					}
					else
					{
						context.lineTo(XX, Y);
					}
				}

				for(let m = 0; m < a.length; ++m)
				{

					let h = a[m] * RcpFT[m];
					if(h > 1.1)
					{
						console.log("should not happen ", a[m], RcpFT[m], h);
						debugger;
						h = 1;
						//todo...
					}
					let hm = h + Last[m];
					if(hm > 1.1)
					{
						console.log("should not happen ", hm, h, Last[m]);
						debugger;
						hm = 1;
						//todo...
					}
					Y = Math.max(YStart - hm * gh, hstart);
					context.lineTo(X, Y);
					X += fWidth;
					Last[m] = hm;
				}
				context.fill();

			}
			context.fillStyle = 'wheat';
			context.textAlign='right';
			context.fillText('100%', nWidth, hstart + FontHeight);
			context.textAlign='left';
			context.fillText(TI.n, 0, hstart + FontHeight);
			hstart += gh;
		}
	}

	if(HighlightKey != 0 && SubMenuActive == -1)
	{
		ToolTipCallback = function(canvas, x, y)
		{
			let TI = ThreadInfo[HighlightKey];
			let ret = [];
			let colors = [];
			for(let l = 0; l < TI.a.length; ++l)
			{
				let a = TI.a[l];
				let idx = GetTimer(TI.ids[l]);
				let c = TimerArray[idx].color;
				colors.push(c);
				ret.push(TimerArray[idx].name);
				let t = FormatTime(a[HighlightFrame]) + 'ms';
				colors.push('white');
				ret.push(t);
			}
			return {c:colors, a:ret};
		}
	}

	ProfileLeave();
}

function DrawGraphPercentile(View, LocalMouseX, LocalMouseY, SubIndex)
{
	let TimerMap = FrameData.TimerMap;
	if(!TimerMap)
		return;

	ProfileEnter("DrawGraphPercentile");
	let Canvas = View.Canvas[View.BackBuffer];
	let context = Canvas.getContext('2d');
	context.clearRect(0, 0, View.w, View.h);

	let h = View.h;
	let w = View.w;
	let NumGraphs = 0;
	let ToolTips = Array();
	for(let key in TimerMap)
	{
		if(!IsGroup(key))
		{
			let idx = GetTimer(key);
			if(TimerArray[idx].e)
			{
				NumGraphs++;
			}
		}
	}
	if(NumGraphs)
	{
		let hstart = 0;
		let gh = h / NumGraphs;
		let Keys = [];
		let cidx = 1;

		if(LocalMouseX >= 0 && LocalMouseY >= 0 && LocalMouseX < w && LocalMouseY < h && SubMenuActive == -1)
		{
			ToolTipCallback = function(canvas, x, y)
			{
				let TimerMap = FrameData.TimerMap;
				let context = canvas.getContext('2d');
				context.font = Font;
				let XPos = x - 20;
				for(let key in TimerMap)
				{
					let idx = GetTimer(key);
					let TimerState = TimerMap[key];
					let T = TimerArray[idx];
					if(!IsGroup(key) && T.e && TimerState.PercentileMax > TimerState.PercentileMin)
					{
						let HighlightIndex = -1;
						let Max = TimerState.PercentileMax;
						let Min = TimerState.PercentileMin;
						let h = View.h;
						let w = View.w;
						let tooltipstring = TimerState.tooltipstring;

						if(LocalMouseX >= 0 && LocalMouseY >= 0 && LocalMouseX < w && LocalMouseY < h && SubMenuActive == -1 && tooltipstring)
						{
							if(TimerState.tooltipysoft)
							{
								let RATE = 0.05;
								if(Math.abs(TimerState.tooltipysoft - TimerState.tooltipy) > 6)
								{
									TimerState.tooltipysoft = TimerState.tooltipy * RATE + TimerState.tooltipysoft * (1-RATE);
								}
							}
							else
							{
								TimerState.tooltipysoft = TimerState.tooltipy;
							}
							let Y = TimerState.tooltipysoft;
							let wtext = context.measureText(tooltipstring, XPos, Y).width;
							let X = Math.max(0, XPos - wtext);
							context.fillStyle = 'black';
							context.fillRect(X - 1, Y-1 , wtext+2, BoxHeight+2);
							context.fillStyle = 'white';
							context.textAlign = 'right';
							context.fillText(tooltipstring, X + wtext, Y+BoxHeight-2);

						}
					}
					context.textAlign = 'left';
				}
			}
		}


		for(let key in TimerMap)
		{
			let idx = GetTimer(key);
			let TimerState = TimerMap[key];
			TimerState.tooltipstring = null;
			let Valid = TimerState.PercentileMax > TimerState.PercentileMin;
			if(!IsGroup(key) && TimerArray[idx].e)
			{
				let Max = Valid ? TimerState.PercentileMax : 1;
				let Min = Valid ? TimerState.PercentileMin : 0;
				let SubGraphSettings = GetSubGraphSettings(key);
				let Percentile = 0.0;
				if(Percentile == null)
					Percentile = 0.0;
				Percentile = Math.max(0.0, Math.min(99.0, Percentile));
				if(!SubGraphSettings.AutomaticReference)
				{
					Max = SubGraphSettings.ReferenceTime;
				}
				let Reference = Max;
				let PercentileData = TimerState.Percentile;
				let PercentileCount = TimerState.PercentileCount;
				let BasePrc = 0;
				if(PercentileCount > PERCENTILE_SAMPLES)
				{
					BasePrc = 100 * (1- PERCENTILE_SAMPLES / PercentileCount);
				}
				let Total = PercentileData.length-1;
				let NumElementsOnScreen = Total * (100 - Percentile) / (100);
				let WidthPerElement = w / NumElementsOnScreen;
				let TotalWidth = Total * WidthPerElement;
				let PercentilePrc = Percentile / 100;
				let PercentileOffset = PercentilePrc * TotalWidth;
				let fHeightScale2 = gh / Max;
				let color = TimerArray[idx].color;
				let X = 0;
				let Y = hstart + gh;
				let YStart = Y;
				let MouseInside = LocalMouseX >= 0 && LocalMouseY >= 0 && LocalMouseX < w && LocalMouseY < h && SubMenuActive == -1;

				context.globalAlpha = 1;
				context.fillStyle = nBackColorsDark[cidx];
				cidx = 1-cidx;
				context.fillRect(0, hstart, w, gh);
				context.strokeStyle = color;
				context.fillStyle = color;
				let PercentileStart = Math.max(0, Math.floor(0.01*Percentile / PercentileData.length));
				{
					context.beginPath();
					context.moveTo(X,Y);
					for(let i = PercentileStart; i < PercentileData.length; ++i)
					{
						X = i * WidthPerElement - PercentileOffset;
						Y = Math.max(YStart - PercentileData[i] * fHeightScale2, hstart);
						context.lineTo(X, Y);
					}
					context.stroke();
					context.lineTo(X, YStart);
					context.globalAlpha = GRAPH_ALPHA;
					context.fill();
				}
				context.globalAlpha = 1;
				context.fillStyle = 'wheat';
				context.textAlign='right';
				context.fillText(FormatTime(Reference) + 'ms', nWidth, hstart + FontHeight);
				context.fillText('100%', nWidth, hstart + gh-2);
				context.textAlign='centered';
				Percentile = BasePrc;
				context.fillText( ((Percentile + 100)/2).toFixed(2) + '%', nWidth/2, hstart + gh-2);
				context.textAlign='left';
				context.fillText(Percentile.toFixed(2) + "%  [Samples:" + PercentileCount + "]", 0, hstart + gh-2);
				context.fillText(TimerArray[idx].name, 15, hstart + FontHeight);
				context.fillText(Percentile + '%', nWidth, hstart + FontHeight);

				if(MouseInside)
				{
					let Element = (PercentileOffset + LocalMouseX) / WidthPerElement;
					let Rounded = Math.round(Element); 
					let HighlightIndex = Math.max(0, Math.min(Rounded, PercentileData.length-1));
					X = HighlightIndex * WidthPerElement - PercentileOffset;
					let Y = YStart - Math.min(PercentileData[HighlightIndex], Reference) * fHeightScale2;
					context.strokeStyle = color;
					context.beginPath();
					let CrossX = X;
					let CrossY = Y;
					context.moveTo(CrossX-2, CrossY-2);
					context.lineTo(CrossX+2, CrossY+2);
					context.moveTo(CrossX+2, CrossY-2);
					context.lineTo(CrossX-2, CrossY+2);
					context.moveTo(CrossX, hstart);
					context.lineTo(CrossX, hstart + gh);
					context.stroke();
					TimerState.tooltipy = Math.min(YStart - BoxHeight, Y) + View.y;
					let Perc = 0;
					if(PERCENTILE_SAMPLES < PercentileCount)
					{
						let Idx = PercentileCount - PERCENTILE_SAMPLES + HighlightIndex;
						Perc = 100 * Idx / (PercentileCount-1);
					}
					else
					{
						let Idx = HighlightIndex - (PERCENTILE_SAMPLES - PercentileCount);
						Idx = Math.max(Idx, 0);
						Perc = 100 * (Idx / (PercentileCount-1))
					}
					TimerState.tooltipstring = FormatTime(Perc) + '% ' + FormatTime(PercentileData[HighlightIndex]) + 'ms';;
				}
				hstart += gh;
			}
		}
	}
	ProfileLeave();
}



function DrawGraph(View, LocalMouseX, LocalMouseY, SubIndex, Split)
{
	var TimerMap = FrameData.TimerMap;
	if(!TimerMap)
		return;

	ProfileEnter("DrawGraph");
	var Canvas = View.Canvas[View.BackBuffer];
	var context = Canvas.getContext('2d');
	context.clearRect(0, 0, View.w, View.h);

	var h = View.h;
	var w = View.w;
	var fHeightScale = h / ReferenceGraph;
	var MouseDragging = 0;
	var fWidth = w / FRAME_COUNT;
	var HighlightFrame = -1;
	var GraphKey = null;
	var GraphBest = 0;
	var MouseTime = ReferenceGraph * (1-(LocalMouseY / h));

	if(LocalMouseX >= 0 && LocalMouseY >= 0 && LocalMouseX < w && LocalMouseY < h && SubMenuActive == -1)
	{
		var index = Math.floor(FRAME_COUNT * LocalMouseX / w);
		HighlightFrame = index;
		for(var key in TimerMap)
		{
			var idx = GetTimer(key);
			var T = TimerArray[idx];
			var TimerState = TimerMap[key];
			var Time = TimerState.Time;
			if(Time[index] >= MouseTime && (GraphBest == 0 || Time[index] <= GraphBest))
			{
				GraphKey = key;
				GraphBest = Time[index];
			}
		}
		ToolTipCallback = function(canvas, x, y)
		{
			if(Split)
			{
				var TimerMap = FrameData.TimerMap;
				var context = canvas.getContext('2d');
				context.font = Font;
				var XPos = x - 20;
				for(var key in TimerMap)
				{
					var idx = GetTimer(key);
					var T = TimerArray[idx];
					var TimerState = TimerMap[key];
					var Time = TimerState.Time;
					if(TimerState.tooltipysoft)
					{
						var RATE = 0.05;
						if(Math.abs(TimerState.tooltipysoft - TimerState.tooltipy) > 6)
						{
							TimerState.tooltipysoft = TimerState.tooltipy * RATE + TimerState.tooltipysoft * (1-RATE);
						}
					}
					else
					{
						TimerState.tooltipysoft = TimerState.tooltipy;
					}
					var Y = TimerState.tooltipysoft;
					var str = '' + FormatTime(Time[index]) + 'ms';
					var w = context.measureText(str, XPos, Y).width;
					var X = Math.max(0, XPos - w);
					context.fillStyle = 'black';
					context.fillRect(X - 1, Y-1 , w+2, BoxHeight+2);
					context.fillStyle = 'white';
					context.textAlign = 'right';
					context.fillText(str, X + w, Y+BoxHeight-2);

				}
				context.textAlign = 'left';
			}
			else
			{
				var StringArray = [];
				var ColorArray = [];
				var TimerMap = FrameData.TimerMap;
				for(var key in TimerMap)
				{
					if(!IsGroup(key))
					{
						var idx = GetTimer(key);
						var T = TimerArray[idx];
						var TimerState = TimerMap[key];
						var Time = TimerState.Time;
						ColorArray.push(TimerArray[idx].color);
						StringArray.push('' + T.name);
						ColorArray.push('white');
						StringArray.push('' + FormatTime(Time[index]) + 'ms') ;
					}
				}
				return {c:ColorArray,a:StringArray};	
			}
		}

	}

	if(Split)
	{
		var NumGraphs = 0;
		for(var key in TimerMap)
		{
			if(!IsGroup(key))
			{
				NumGraphs++;
			}
		}
		if(NumGraphs)
		{
			var hstart = 0;
			var gh = h / NumGraphs;
			var Keys = [];
			var cidx = 1;
			for(var key in TimerMap)
			{
				var idx = GetTimer(key);
				var TimerState = TimerMap[key];
				if(!IsGroup(key))
				{
					let SubGraphSettings = GetSubGraphSettings(key);
					var Reference = GetSubGraphReferenceTime(SubGraphSettings, TimerState);
					var fHeightScale2 = gh / Reference;

					var Time = TimerState.Time;
					var color = TimerArray[idx].color;
					var X = w - Time.length*fWidth;
					var Y = hstart + gh;
					var YStart = Y;
					context.globalAlpha = 1;
					context.fillStyle = nBackColorsDark[cidx];
					cidx = 1-cidx;
					context.fillRect(0, hstart, w, gh);
					context.strokeStyle = color;
					context.fillStyle = color;
					context.beginPath();
					context.moveTo(X,Y);
					for(var i = 0; i < Time.length; ++i)
					{
						Y = Math.max(YStart - Time[i] * fHeightScale2, hstart);
						context.lineTo(X, Y);
						X += fWidth;

					}
					context.stroke();
					context.lineTo(X, YStart);
					context.globalAlpha = GRAPH_ALPHA;
					context.fill();


					context.globalAlpha = 1;
					context.fillStyle = 'wheat';
					context.textAlign='right';
					context.fillText(FormatTime(Reference) + 'ms', nWidth, hstart + FontHeight);
					context.textAlign='left';
					context.fillText(TimerArray[idx].name, 15, hstart + FontHeight);

					if(HighlightFrame >= 0)
					{
						var X = w - Time.length * fWidth + fWidth * HighlightFrame;
						var Y = YStart - Math.min(Time[HighlightFrame], Reference) * fHeightScale2;
						context.strokeStyle = color;
						context.beginPath();
						var CrossX = X;
						var CrossY = Y;
						context.moveTo(CrossX-2, CrossY-2);
						context.lineTo(CrossX+2, CrossY+2);
						context.moveTo(CrossX+2, CrossY-2);
						context.lineTo(CrossX-2, CrossY+2);
						context.moveTo(CrossX, hstart);
						context.lineTo(CrossX, hstart + gh);
						context.stroke();
						TimerState.tooltipy = Math.min(YStart - BoxHeight, Y) + View.y;
					}
					hstart += gh;
				}
			}
		}
	}
	else
	{
		var Keys = [];
		var Prev = Array();
		var He = Array();
		for(var key in TimerMap)
		{
			if(!IsGroup(key))
			{
				var idx = GetTimer(key);
				var TimerState = TimerMap[key];
				var Time = TimerState.Time;
				var color = TimerArray[idx].color;
				var X = w - Time.length*fWidth;
				var Y = h;
				while(Time.length > He.length)
				{
					He.push(0.0);
				}
				for(var i = 0; i < Time.length; ++i)
				{
					He[i] += Time[i];
				}
			}
		}
		if(1) // graph with clipping. this code seems way to complicated
		{
			var NumGraphs = 0;
			var Len = 0;
			for(let key in TimerMap)
			{
				if(!IsGroup(key))
				{
					var idx = GetTimer(key);
					var TimerState = TimerMap[key];						
					var Time = TimerState.Time;
					Len = Math.max(Len, Time.length);
					NumGraphs++;
				}
			}
			let g = Array(Len);
			for(let i = 0; i < g.length; ++i)
				g[i] = Array(NumGraphs);
			let i = 0;
			for(let key in TimerMap)
			{
				if(!IsGroup(key))
				{
					var TimerState = TimerMap[key];						
					var Time = TimerState.Time;
					let x = GetTimer(key);
					for(let j = 0; j < Time.length; ++j)
					{
						g[j][i] = {k:key, v: Time[j], x:x, c:TimerArray[x].color,n:TimerArray[x].name};
					}
					i++;
				}
			}
			for(let i = 0; i < g.length; ++i)
			{
				let a = g[i];
				a.sort( function(l, r){
					return l.v-r.v;
				});
			}
			let cvt = function(t){ return h - t * fHeightScale; };
			if(1)
			{
				let Segments = Array();
				let MakeSegment = function(b,e)
				{
					let s = {b:b, e:e, a:[]};
					Segments.push(s);
					return s;
				};
				let MakeEntry = function(y0l, y0h, y1l, y1h, c)
				{
					let e ={y0l:y0l,y0h:y0h,y1h:y1h,y1l:y1l,c:c,t:0};
					return e;
				};

				let X = 0;
				for(let i = 1; i < g.length; ++i)
				{
					let a0 = g[i-1];
					let a1 = g[i];
					let s = MakeSegment(X, X + fWidth);
					if(a0.length != a1.length)
					{
						console.log("should not happen!!");
						debugger;
					}
					let FailIndex = -1;
					for(let j = 0; j < a0.length; ++j)
					{
						if(a0[j].k != a1[j].k)
						{
							FailIndex = j;
							break;
						}

						let t0l = j > 0 ? a0[j-1].v : 0;
						let t0h = a0[j].v;
						let t1l = j > 0 ? a1[j-1].v : 0;
						let t1h = a1[j].v;
						let c = a1[j].c;
						let e = MakeEntry(t0l, t0h, t1l, t1h, c);
						s.a.push(e); 
					}
					if(FailIndex >= 0)
					{
						let l = a0.length;
						let ax = Array(l);
						if(FailIndex + 1 >= l)
							debugger;///no lines, should not happen.
						for(let j = FailIndex; j < l; ++j)
						{
							//find matching
							for(let k = FailIndex; k < l; ++k)
							{
								if(a1[k].k == a0[j].k)
								{
									if(ax[j])
									{
										console.log("should never happen");
										debugger;//should not happen
									}
									ax[j] = a1[k];
									break;
								}
							}
						}
						//find all intersections within [0-1]
						let I = [];
						I.push(0);
						for(let j = FailIndex; j < l; ++j)
						{
							let ya = a0[j].v;
							let yam = ax[j].v;
							let ka = yam-ya;
							for(let k = j + 1; k < l; ++k)
							{
								let yb = a0[k].v;
								let ybm = ax[k].v;
								let kb = ybm-yb;
								let num = yb-ya;
								let denom = ka-kb;
								if(denom>0.00001||denom < -0.00001)
								{
									let x = num/denom;
									if(x>=0 && x <= 1)
									{
										I.push(x);
									}
								}
							}
						}
						I.push(1);
						if(I.length)
						{
							I.push(I[0]);
							I.push(I[0]);
							I.sort();

							let i = 0;
							let DIST = 0.00001;
							while(i+1 < I.length)
							{
								if(Math.abs(I[i] - I[i+1]) < DIST)
								{
									I.splice(i, 1);
								}
								else
								{
									i++;
								}
							}
						}
						for(let i = 0; i < I.length-1; ++i)
						{
							let x0 = I[i];
							let x1 = I[i+1];
							let xm =  0.5 * (x0+x1);
							let b = X + x0 * fWidth;
							let e = X + x1 * fWidth;
							let c = X + xm * fWidth;
							let s = MakeSegment(b, e);
							for(let j = FailIndex; j < l; ++j)
							{
								let y = a0[j].v;
								let yx = ax[j].v;
								let yd = yx - y;
								let y0 = y + yd * x0;
								let y1 = y + yd * x1;
								let ym = y + yd * xm;
								let e = MakeEntry(ym, y0, ym, y1, a0[j].c);
								s.a.push(e); 
							}
							s.a.sort(function(l, r){
								return l.y0l-r.y0l;
							});
							for(let j = 0; j < s.a.length; ++j)
							{
								if(j == 0)
								{
									s.a[j].y0l = 0;
									s.a[j].y1l = 0;
								}
								else
								{
									s.a[j].y0l = s.a[j-1].y0h;
									s.a[j].y1l = s.a[j-1].y1h;
								}
							}
						}
					}
					X += fWidth;
				}
				for(let i = 0; i < Segments.length; ++i)
				{
					let s = Segments[i];
					let X0 = s.b;
					let X1 = s.e;
					for(let j = 0; j < s.a.length; ++j)
					{
						let e = s.a[j];
						let xs = [];
						let ytop = [];
						let ybot = [];
						if(!e.t)
						{
							e.t = 1;
							let XB = X0;
							let XE = X1;
							xs.push(XB);
							ytop.push(cvt(e.y0h));
							ybot.push(cvt(e.y0l));
							let yeh = e.y1h;
							let yel = e.y1l;
							let k = i + 1;
							let Found = true;
							while(k < Segments.length && Found)
							{
								Found = false;
								let s = Segments[k];
								for(let l = 0; l < s.a.length && !Found; ++l)
								{
									let e1 = s.a[l];
									if(!e1.t && e1.c == e.c && Math.abs(e1.y0l-e.y1l) < 0.0001 && Math.abs(e1.y0h - e.y1h) < 0.0001)
									{
										e1.t = 1;
										xs.push(XE);
										ytop.push(cvt(yeh));
										ybot.push(cvt(yel));
										XE = s.e;
										yeh = e1.y1h;
										yel = e1.y1l;
										Found = true;
										e = e1;
									}
								}
								k++;
							}
							xs.push(XE);
							ytop.push(cvt(yeh));
							ybot.push(cvt(yel));
						}
						if(xs.length)
						{
							context.strokeStyle = e.c;
							context.fillStyle = e.c;
							context.globalAlpha = GRAPH_ALPHA;
							context.beginPath();
							context.moveTo(xs[0], ytop[0]);
							for(let k = 1; k < xs.length; ++k)
							{
								context.lineTo(xs[k], ytop[k]);
							}
							for(let k = xs.length-1; k >= 0; --k)
							{
								context.lineTo(xs[k], ybot[k]);
							}
							context.fill();
							context.beginPath();
							context.moveTo(xs[0], ytop[0]);
							for(let k = 1; k < xs.length; ++k)
							{
								context.lineTo(xs[k], ytop[k]);
							}
							context.globalAlpha = 1.0;
							context.stroke();
						}
					}
				}
			}	
		}
		if(0) //old graph code, no clipping
		{
			for(var key in TimerMap)
			{
				if(!IsGroup(key))
				{
					var idx = GetTimer(key);
					var TimerState = TimerMap[key];
					var Time = TimerState.Time;
					var color = TimerArray[idx].color;
					var X = w - Time.length*fWidth;
					var Y = h;
					context.strokeStyle = color;
					context.fillStyle = color;

					context.beginPath();
					context.moveTo(X,Y);
					for(var i = 0; i < Time.length; ++i)
					{
						Y = h - Time[i] * fHeightScale;
						context.lineTo(X, Y);
						X += fWidth;
					}
					context.stroke();
					context.lineTo(X, h);
					context.globalAlpha = GRAPH_ALPHA;
					context.fill();
					context.globalAlpha = 1;
				}
			}
		}
		if(HighlightFrame >= 0)
		{
			for(var key in TimerMap)
			{
				if(!IsGroup(key))
				{
					var idx = GetTimer(key);
					var TimerState = TimerMap[key];
					var Time = TimerState.Time;
					var color = TimerArray[idx].color;
					var X = w - Time.length*fWidth + fWidth * HighlightFrame;
					var Y = h - Time[HighlightFrame] * fHeightScale;
					context.strokeStyle = color;
					context.beginPath();
					var CrossX = X;
					var CrossY = Y;
					context.moveTo(CrossX-2, CrossY-2);
					context.lineTo(CrossX+2, CrossY+2);
					context.moveTo(CrossX+2, CrossY-2);
					context.lineTo(CrossX-2, CrossY+2);
					context.stroke();
				}
			}
		}
		context.fillStyle = 'wheat';
		context.textAlign='right';
		context.fillText(FormatTime(ReferenceGraph) + 'ms', nWidth, FontHeight);
		context.textAlign='left';
	}

	ProfileLeave();
}

function StringHash(s) //note: matching code in microprofile.cpp: uint32_t MicroProfileStringHash(const char* pString)
{ 
	var h = 0xfeedba3e;
	for(var i = 0; i < s.length; ++i)
	{
		h = s.charCodeAt(i) + ((h << 5) - h);
		h = h & h;
	}
	return Math.abs(h);
}

function StringColorIndex(Name)
{
	var h = StringHash(Name);
	var cidx = Math.floor(360*(h  / (1<<32-1)) );
	return cidx;
}

function ColorFromString(Name, S, L)
{
	var H = StringColorIndex(Name);
	return "hsl(" + H + "," + S + "%, " + L+ "%)";
}

function LerpColor(v)
{
	var R_0 = 0;
	var G_0 = 1;
	var B_0 = 0;

	var R_1 = 1;
	var G_1 = 0.5;
	var B_1 = 0;

	var R_2 = 1;
	var G_2 = 0;
	var B_3 = 0;
	var R;
	var G;
	if(v < 0.5)
	{
		v *= 2;
		var v0 = (1-v);
		R = R_0 * v0 + R_1 * v;
		G = G_0 * v0 + G_1 * v;

	}
	else
	{
		v = (v-0.5) * 2;
		var v0 = (1-v);
		R = R_1 * v0 + R_2 * v;
		G = G_1 * v0 + G_2 * v;
	}
	R *= 255;
	G *= 255;
	return "rgb(" + R.toFixed(0) + "," + G.toFixed(0) + ",0)";

}

function DrawRange(context, X, XEnd, Y, YEnd, ColorBack, ColorFront)
{
	if(X < XEnd)
	{
		var W = XEnd - X;
		var H = YEnd - Y;
		context.globalAlpha = 0.1;
		context.fillStyle = ColorBack;
		context.fillRect(X, Y, W, H);
		context.globalAlpha = 1;
		context.strokeStyle = ColorFront;
		context.beginPath();
		context.moveTo(X, 0);
		context.lineTo(X, H);
		context.moveTo(X+W, 0);
		context.lineTo(X+W, H);
		// context.closePath();
		context.stroke();
	}
}


function DrawHistory(View, LocalMouseX, LocalMouseY)
{
	ProfileEnter("DrawHistory");
	var Canvas = View.Canvas[View.BackBuffer];
	var context = Canvas.getContext('2d');
	context.clearRect(0, 0, View.w, View.h);
	if(!FrameData.Time)
		return;
	var fHeight = View.h;
	var fWidth = nWidth / FRAME_COUNT;
	var fHeightScale = fHeight / ReferenceHistory;
	var fX = 0;
	var FrameIndex = -1;
	var MouseDragging = 0;
	var GreenTime = (Settings.TargetTime * 0.9);
	var RedBegin = (Settings.TargetTime * 1.1);
	var LerpDist = 1.0 / (RedBegin - GreenTime);
	var id0 = -1;
	var id1 = -1;
	
	if(MouseDragActiveXEnd > MouseDragActiveXStart)
	{
		var idx0 = Math.ceil(FRAME_COUNT * MouseDragActiveXStart / nWidth);
		var idx1 = Math.floor(FRAME_COUNT * MouseDragActiveXEnd / nWidth);
		idx0 = Clamp(idx0, 0, FRAME_COUNT-1);
		idx1 = Clamp(idx1, 0, FRAME_COUNT-1);
		id0 = FrameData.Ids[idx0];
		id1 = FrameData.Ids[idx1];
	}


	var ToolTipFrame = -1;
	for(var i = 0; i < FRAME_COUNT; i++)
	{
		var fMs = FrameData.Time[i];
		var fPrc = (fMs - GreenTime) * LerpDist;
		fPrc = Clamp(fPrc, 0, 1);
		var color = LerpColor(fPrc);
		var fid = FrameData.Ids[i];
		if(fid >= id0 && fid <= id1)
		{
			color = 'cyan';
		}else if(FrameData.Frozen[i])
		{
			color = 'purple';
		}


		var fH = fHeightScale * fMs;
		var bMouse = LocalMouseX > fX && LocalMouseX < fX + fWidth+1 && MouseY <= HistoryHeight;
		if(bMouse && !MouseDragging)
		{
			context.fillStyle = FRAME_HISTORY_COLOR_GPU;
			ToolTipFrame = i;
		}
		else
		{
			context.fillStyle = color;
		}
		context.fillRect(fX, fHeight - fH, fWidth-1, fH);
		fX += fWidth;
	}


	DrawRange(context, MouseDragActiveXStart, MouseDragActiveXEnd, 0, HistoryHeight, '#59d0ff', '#00ddff');


	var fH = fHeight - fHeightScale * Settings.TargetTime;
	context.fillStyle = 'wheat';
	context.strokeStyle = 'wheat';
	context.beginPath();
	context.moveTo(0, fH);
	context.lineTo(nWidth, fH);
	// context.closePath();
	context.stroke();
	var YText;
	if(fH > HistoryHeight * 0.25)
	{
		YText = fH - FontAscent;
	}
	else
	{
		YText = fH + FontHeight;
	}

	context.fillText(Settings.TargetTime + 'ms', 3, YText);
	context.textAlign='right';
	context.fillText(FormatTime(ReferenceHistory) + 'ms', nWidth, FontHeight);
	context.textAlign='left';



	if(ToolTipFrame >= 0)
	{
		var fMs = FrameData.Time[ToolTipFrame];
		var Frozen = FrameData.Frozen[ToolTipFrame];
		ToolTipCallback = function()
		{
			var StringArray = [];
			StringArray.push("Frame");
			StringArray.push("" + ToolTipFrame);
			StringArray.push("Time");
			StringArray.push("" + fMs.toFixed(3));
			if(Frozen)
			{
				StringArray.push("Frozen & Unreliable");
				StringArray.push("");
			}
			return StringArray;
		}
	}


	ProfileLeave();

}
function MouseInRect(Rect)
{
	return MouseInside(Rect.x, Rect.y, Rect.w, Rect.h);
}
function MouseInside(X, Y, W, H)
{
	return MouseX >= X && MouseX <= X + W && MouseY >= Y && MouseY <= Y + H;
}

var MessageText = "";
var MessageTimeout = -1;
var MessageTimeoutLast = new Date();
var MessageShowSpinner = 0;
function SetMessage(text, TimeOut, ShowSpinner)
{
	if(TimeOut)
	{
		MessageTimeout = TimeOut;
	}
	else
	{
		MessageTimeout = -1;
	}
	MessageText = text;
	MessageShowSpinner = ShowSpinner;
}
function ClearMessage(Message)
{
	if(Message == MessageText)
	{
		MessageText = "";
		MessageTimeout = -1;
	}
}

function DrawMessage()
{
	var context = CanvasDetailedView.getContext('2d');
	var Now = new Date();
	var Delta = Now - MessageTimeoutLast;
	if(MessageTimeout>0)
	{
		MessageTimeout -= Delta;
		if(MessageTimeout<= 0)
		{
			MessageText = "";
			MessageTimeout = -1;
		}

	}
	MessageTimeoutLast = Now;

	var Text = MessageText;
	var X = nWidth / 2;

	var Y = nHeight / 2;
	context.font = FontFlash;
	context.textAlign = 'center';
	context.fillStyle = 'red';

	function MSG(a, Spinner)
	{
		context.fillText(a, X, Y);
		if(Spinner)
		{
			var w = context.measureText(a).width;
			SpinnerDraw(1, context, SpinnerText0, X + 3 + w*0.5, Y - 25,  30, 30);
			SpinnerDraw(1, context, SpinnerText1, X - 3 - w*0.5 - 30, Y - 25,  30, 30);
		}
		Y -= 60;
	}
	if(!HelpFade)
		HelpFade = new Date();
	var HelpFadeTime = new Date() - HelpFade;
	if(HelpFadeTime < 2000)
	{
		var Alpha = 1 - (HelpFadeTime/2000);
		context.globalAlpha = Alpha;
		context.fillText("Press 'h' for help", X, 200);
		context.globalAlpha = 1;
	}

	if(Text != "")
	{
		MSG(Text);
	}
	if(IsFrozen)
	{
		MSG("FROZEN[space]");
	}

	PresetPending++; //hack: wait 20 frames before showing enable messages to prevent it from showing when loading settings. [[[test]]]
	if(WSIsOpen && PresetPending > 20)
	{
		if(Settings.ViewActive != VIEW_COUNTERS)
		{
			if(GroupsEnabled == 0)
			{
				MSG("Paused: Enable groups in 'Control' menu to unpause");
			}
			if(TimersEnabled == 0 && Settings.ViewActive != VIEW_GRAPH_THREAD_GROUP)
			{
				MSG("Enable Timers or Functions");
			}
		}
	}
	context.textAlign = 'left';
	context.font = Font;
}

function DrawGraphSettingsMenu(context, XBase, Y, width, height)
{
	if(SubMenuActive != SubMenuGraphSettings)
	{
		SubMenuGraphSettingsIndex = -1;
		SubMenuGraphSettingsKey = "";
	}
	if(!ShowMenu())
	{
		return;
	}
	if(!(Settings.ViewActive == VIEW_GRAPH_SPLIT || Settings.ViewActive == VIEW_GRAPH_PERCENTILE))
	{
		return;
	}
	let NumGraphs = 0;
	let TimerMap = FrameData.TimerMap;

	for(let key in TimerMap)
	{
		if(!IsGroup(key))
		{
			let idx = GetTimer(key);
			if(TimerArray[idx].e)
			{
				NumGraphs++;
			}
		}
	}
	let h = height;
	let w = width;
	let hstart = Y;
	let gh = h / NumGraphs;
	NumGraphs = 0;
	for(let key in TimerMap)
	{
		if(!IsGroup(key))
		{
			let idx = GetTimer(key);
			if(TimerArray[idx].e)
			{
				let X = XBase+1;
				let Y = hstart+1;
				let MenuText = ">";
				let w2 = 2 + context.measureText(MenuText).width;
				let bMouseInside = MouseInside(X, Y, w2, 4 + FontHeight);
				context.fillStyle = bMouseInside || (SubMenuGraphSettings == SubMenuActive && SubMenuGraphSettingsIndex == NumGraphs) ? nBackColors[1] : 'black';
				context.fillRect(X, Y, w2, 4 + FontHeight);
				context.fillStyle = '#ffffff';
				context.fillText(MenuText, X, Y + FontHeight);
				context.textAlign = 'left';
				if(bMouseInside)
				{
					SubMenuGraphSettingsIndex = NumGraphs;
					SubMenuGraphSettingsKey = key;
					CaptureButtonX = X + w2 + 2;
					CaptureButtonY = Y;
					EnableMenu(SubMenuGraphSettings);
				}
				NumGraphs++;
				hstart += gh;


			}
		}
	}
}

function DrawCaptureMenu(context)
{
	MouseInCaptureButton = 0;
	if(!ShowMenu())
	{
		return;
	}

	var CaptureRange = MouseDragActiveXStart < MouseDragActiveXEnd ? "Selection" : (""+Settings.CaptureFrames);
	var CaptureText = "Capture[" + CaptureRange + "]";
	var w = 10 + context.measureText(CaptureText).width;
	var X = nWidth / 2 - w / 2;
	var XCenter = nWidth / 2;
	var Y = nHeight - 30;
	MouseInCaptureButton = MouseInside(X, Y, w, 4 + FontHeight);
	context.textAlign = 'center';
	context.fillStyle = MouseInCaptureButton ? nBackColors[1] : 'black';
	context.fillRect(X, Y, w, 4 + FontHeight);
	context.fillStyle = '#ffffff';
	context.fillText(CaptureText, XCenter, Y + FontHeight);



	X += w + 2;
	var MenuText = "^";
	var w2 = 10 + context.measureText(MenuText).width;
	var bMouseInCaptureMenu = MouseInside(X, Y, w2, 4 + FontHeight);
	context.fillStyle = bMouseInCaptureMenu ? nBackColors[1] : 'black';
	context.fillRect(X, Y, w2, 4 + FontHeight);

	context.fillStyle = '#ffffff';
	context.fillText(MenuText, X + w2*0.5, Y + FontHeight);

	let X1 = X + w2 + 10;
	if(CaptureTriggerDelta)
	{
		let text = FormatTime(CaptureTriggerDelta);
		let w = 10 + context.measureText("text").width;
		context.fillStyle = 'black';
		context.fillRect(X1, Y, w, 4 + FontHeight);
		context.fillStyle = '#ffffff';
		context.fillText(text, X1 + w*0.5, Y + FontHeight);	
		X1 += w;
	}

	let Pending = (CaptureTriggerTime != null && CaptureTriggerTimeType == 2);
	if(AutoCaptureEnabled || Pending)
	{
		let d = Dots();
		let AutoStatus = Pending ? (" pending" + Dots()) : "";
		let Source = GetAutoCaptureString();
		let Threshold = FormatTime(Settings.AutoCaptureTheshold);
		let str = "Autocapture " + AutoStatus + " Source:'" + Source + "' Threshold:" + Threshold + "ms Repeat=" + (Pending?Settings.AutoCaptureRepeat:AutoCaptureEnabled);
		let w = 10 + context.measureText(str).width;
		context.fillStyle = 'black';
		context.fillRect(X1, Y, w, 4 + FontHeight);
		context.fillStyle = '#ffffff';
		context.fillText(str, X1 + w*0.5, Y + FontHeight);	
		X1 += w;
	}

	context.textAlign = 'left';
	if(bMouseInCaptureMenu)
	{
		CaptureButtonX = X + w2;
		CaptureButtonY = Y;
		EnableMenu(SubMenuCapture);
	}	
}
function UpdateX7Views()
{
	if(Settings.ViewActive == VIEW_BAR_SINGLE || Settings.ViewActive == VIEW_BAR_ALL)
	{
		var NumSubViews = 0;
		var BarColumnEnabled = GetBarColumnEnabled();
		X7BarFirstView = -1;
		X7BarLastView = -1;
		var ViewMask = 0;
		for(var i = 0; i < BarColumnEnabled.length; ++i)
		{
			if(BarColumnEnabled[i])
			{
				if(X7BarFirstView == -1)
					X7BarFirstView = i;
				X7BarColumnRemap[NumSubViews++] = i;
				ViewMask = ViewMask | (1 << i);
				X7BarLastView = i;
			}
		}
		if(ViewMask != X7BarColumnMask)
		{
			console.log("resizing views");
			var w = NumSubViews ? nWidth / NumSubViews : 1;
			for(var i = 0; i < X7Views.length; ++i)
			{
				X7Views[i].visible = i < NumSubViews;
				if(i < NumSubViews)
				{
					var HistoryH = Settings.ViewCompressed ? 0 : HistoryHeight;
					ResizeView(X7Views[i], w*i, HistoryH, w, nHeight - HistoryH);
				}
			}
			X7BarColumnMask = ViewMask;
		}
		X7LegendView.visible = true;
		ReferenceBarAutomatic = 0;
	}
}
function DrawViews()
{
	Plotf("DrawViews");
	UpdateX7Views();
	ProfileEnter("UpdateViews");
	ViewIndex = 0;
	for(var i = 0; i < Views.length; ++i)
	{
		var View = Views[i];
		if(View.visible)
		{
			var LocalMouseX = MouseX - View.x;
			var LocalMouseY = MouseY - View.y;
			View.DisplayFunc(View, LocalMouseX, LocalMouseY, View.index);
			var Canvas = View.Canvas[View.BackBuffer];
			var Context = Canvas.getContext('2d');
			ViewIndex++;
		}
	}
	ProfileLeave();
	var context = CanvasDetailedView.getContext('2d');
	context.clearRect(0, 0, nWidth, nHeight);
	ProfileEnter("BlitViews");
	for(var i = 0; i < Views.length; ++i)
	{
		var View = Views[i];
		if(View.visible)
		{
			context.drawImage(View.Canvas[View.BackBuffer], Math.floor(View.x), Math.floor(View.y), View.w, View.h);
		}
	}

	DrawCaptureMenu(context);
	DrawGraphSettingsMenu(context, MainView.x, MainView.y, MainView.w, MainView.h);

	ProfileLeave();

}
var SubMenuGroup = 0;
var SubMenuTimers = 1;
var SubMenuModules = 2;
var SubMenuFunctions = 3;
var SubMenuPatched = 4;
var SubMenuSettings = 5;
var SubMenuViews = 6;
var SubMenuPresets = 7;
var SubMenuColumns = 8;
var SubMenuCapture = 9;
var SubMenuGraphSettings = 10;
var SubMenuGraphSettingsIndex = -1;
var SubMenuGraphSettingsKey = "";

var SubMenuActive = -1;
var SubMenuTimeoutBase = 0.7;
var SubMenuMouseX = 0;
var SubMenuMouseY = 0;
var SubMenuTimeout = new Date();
var MenuItems = [];
var FilterInputTimersValue = '';
var FilterInputGroupsValue = '';
var FilterInputModulesValue = '';
var FilterInputFunctionsValue = '';
var FilterInputPatchedValue = '';


function MakeMenuItem(name, f, visible)
{
	var Item = {};
	Item.name = name;
	Item.f = f;
	Item.w = name.length;
	Item.x = 0;
	Item.y = 0;
	Item.visible = visible;
	return Item;
}
function EnableMenu(m)
{
	if(m != SubMenuActive)
	{
		if(SubMenuActive == SubMenuTimers)
		{
			FilterInputTimersValue = FilterInput.value;
		}
		else if(SubMenuActive == SubMenuGroup)
		{
			FilterInputGroupsValue = FilterInput.value;
		}
		else if(SubMenuActive == SubMenuModules)
		{
			FilterInputModulesValue = FilterInput.value;
		}
		else if(SubMenuActive == SubMenuFunctions)
		{
			FilterInputFunctionsValue = FilterInput.value;
		}
		else if(SubMenuActive == SubMenuPatched)
		{
			FilterInputPatchedValue = FilterInput.value;
		}

		SubMenuActive = m;
		SubMenuTimeout = new Date();

		if(SubMenuActive == SubMenuTimers)
		{
			FilterInput.value = FilterInputTimersValue;
		}
		else if(SubMenuActive == SubMenuGroup)
		{
			FilterInput.value = FilterInputGroupsValue;
		}
		else if(SubMenuActive == SubMenuModules)
		{
			FilterInput.value = FilterInputModulesValue;
		}
		else if(SubMenuActive == SubMenuFunctions)
		{
			FilterInput.value = FilterInputFunctionsValue;
		}
		else if(SubMenuActive == SubMenuPatched)
		{
			FilterInput.value = FilterInputPatchedValue;
		}

		FilterInputValueLast = FilterInput.value;
	}
	if(m == -1)
	{
		SubMenuTimeout = 0;
	}

	if(SubMenuActive == SubMenuTimers || SubMenuActive == SubMenuGroup || SubMenuActive == SubMenuModules || SubMenuActive == SubMenuFunctions || SubMenuActive == SubMenuPatched)
	{
		FilterInputDiv.style['display'] = 'inline';
		FilterInput.focus();
	}
	else
	{
		FilterInputDiv.style['display'] = 'none';
	}
}

function Clamp(v, low, high)
{
	return v < low ? low : (v > high ? high : v);
}
function TriggerCapture()
{
	if(Settings.CaptureDelay <= 0)
	{
		Capture();
	}
	else
	{
		CaptureTriggerTime = new Date();
		CaptureTriggerTimeType = 1; 
	}
}
function CaptureUpdate()
{
	if(CaptureTriggerTime)
	{
		let Time = new Date();
		let Delta = Time - CaptureTriggerTime;
		CaptureTriggerDelta = Math.max(0.0, (Settings.CaptureDelay * 1000 - Delta) / 1000.0);
		if(Delta > Settings.CaptureDelay * 1000)
		{
			if(CaptureTriggerTimeType == 1)
			{
				Capture();
			}
			else if(CaptureTriggerTimeType == 2)
			{
				AutoCaptureEnabled = Settings.AutoCaptureRepeat > 0 ? Settings.AutoCaptureRepeat : 1;
			}
			CaptureTriggerTime = null;
			CaptureTriggerDelta = 0;
			CaptureTriggerTimeType = 0;
		}
	}

}
function Capture()
{
	AutoCaptureCooldown = Settings.CaptureFrames + 5;
	var ext = '' + Settings.CaptureFrames;
	if(MouseDragActiveXEnd > MouseDragActiveXStart)
	{
		var idx0 = Math.ceil(FRAME_COUNT * MouseDragActiveXStart / nWidth);
		var idx1 = Math.floor(FRAME_COUNT * MouseDragActiveXEnd / nWidth);
		idx0 = Clamp(idx0, 0, FRAME_COUNT-1);
		idx1 = Clamp(idx1, 0, FRAME_COUNT-1);
		var id0 = FrameData.Ids[idx0];
		var id1 = FrameData.Ids[idx1];
		ext = 'r/'+id0+'/'+id1;
	}
	var url = 'http://' + WSHost + ':' + WSPort + '/' + ext;
	window.open(url);
}
function InitMenu()
{
	MenuItems = [];
	MenuItems.push(MakeMenuItem("Control", function(){EnableMenu(SubMenuGroup); } ));
	MenuItems.push(MakeMenuItem("Timers", function(){EnableMenu(SubMenuTimers); } ));
	MenuItems.push(MakeMenuItem("Modules", function(){EnableMenu(SubMenuModules); } ));
	MenuItems.push(MakeMenuItem("Functions", function(){EnableMenu(SubMenuFunctions); } ));
	MenuItems.push(MakeMenuItem("Patched", function(){EnableMenu(SubMenuPatched); }, function(){ return FunctionsInstrumented.length > 0;} ));
	MenuItems.push(MakeMenuItem("Settings", function(){ EnableMenu(SubMenuSettings); } ));
	MenuItems.push(MakeMenuItem("Views", function(){ EnableMenu(SubMenuViews); } ));
	MenuItems.push(MakeMenuItem("Presets", function(){ EnableMenu(SubMenuPresets); } ));
	MenuItems.push(MakeMenuItem("Columns", function(){ EnableMenu(SubMenuColumns); } ));
}
function DrawTopMenu()
{
	var context = CanvasDetailedView.getContext('2d');
	var X = 2;
	var Y = 0;
	var MouseInY = MouseY < BoxHeight;
	for(var i = 0; i < MenuItems.length; ++i)
	{
		if(i == SubMenuColumns)
		{
			if(Settings.ViewActive == VIEW_GRAPH_SPLIT || 
				Settings.ViewActive == VIEW_GRAPH_THREAD_GROUP ||
				Settings.ViewActive == VIEW_GRAPH_PERCENTILE || 
				Settings.ViewActive == VIEW_COUNTERS)
			{
				continue;
			}
		}
		var Item = MenuItems[i];
		if(Item.visible == null || Item.visible())
		{
			var w = context.measureText(Item.name).width + 4;
			var MouseIn = MouseInY && MouseX >= X && MouseX < X + w;
			var color = MouseIn ? nBackColors[1] : "black";
			Item.x = X;
			Item.y = Y + BoxHeight;
			if(MouseIn)
			{
				context.fillStyle = 'white';
				context.fillRect(X-2, Y, w+4, BoxHeight);
				// Enable
				EnableMenu(i);
			}
			context.fillStyle = color;
			context.fillRect(X, Y, w, BoxHeight);
			context.fillStyle = "white";
			context.fillText(Item.name, X+2, Y+BoxHeight-FontAscent);
			if(MouseIn && MouseReleased)
			{
				Item.f();
			}
			X += w + 6;
		}
	}
}

function AggregateMenuSize()
{
	var w = 250 + 5 + FontWidth;
	return WindowRect(nWidth / 2 - w / 2,HistoryHeight + 50, w, nHeight);
}
function TimerMenuSize()
{
	return MenuSize(WidthArray[TYPE_TIMER] + WidthArray[TYPE_GROUP] + 5 + FontWidth);
}

function GroupMenuSize()
{
	return MenuSize(WidthTree);
}

function MenuSize(w)
{
	return WindowRect(nWidth / 2 - w / 2, HistoryHeight + 50,w, nHeight);
}

function CreateFilter(Filter)
{
	if(!Filter || Filter.length == 0)
	{
		return null;
	}
	Filter = Filter.split(' ');
	
	var regexp = "";
	for(var i = 0; i < Filter.length; ++i)
	{
		regexp = regexp + ".*" + Filter[i];
	}
	Filter = new Array();
	regexp = regexp + ".*";
	Filter.push(new RegExp(regexp, "i"));
	return Filter;
}

function FilterMatch(FilterArray, value)
{
	if(!FilterArray)
		return true;
	for(var i = 0; i < FilterArray.length; ++i)
	{
		var res = value.search(FilterArray[i]);
		if(res<0)
			return false;
	}
	return true;
}

function AddPreset(Name)
{
	var O = {};
	O[Name] = {};
	var OO = {};
	OO.p = O;
	OO.r = {};
	AddPresets(OO);
}
function JSONTryParse(str)
{
	if(typeof str == 'string')
	{
		try{
			return JSON.parse(str);
		}
		catch(e){}
	}
	return {};
}

function ProcessPresets(Data, Names, Cache)
{
	for(var idx in Data)
	{
		if(Names.indexOf(idx) == -1)
		{
			Names.push(idx);
		}
		Cache[idx] = JSONTryParse(Data[idx]);	
	}
}
function AddPresets(Obj)
{
	var Names = Obj.p;
	var ReadOnlyNames = Obj.r;
	ProcessPresets(Obj.p, Presets, PresetsCache);
	ProcessPresets(Obj.r, ReadOnlyPresets, ReadOnlyPresetsCache);
}
function GetFullName(T)
{
	var parent = T.parent;
	var ParentName = "unknown";
	var Name = T.name;
	if(parent && parent < TimerArray.length)
	{
		ParentName = TimerArray[parent].name;
	}
	return ParentName + "/" + Name;
}

function ColorFromFullName(name, type)
{
	for(var i = 0; i < TimerArray.length; ++i)
	{
		var t = TimerArray[i];
		if(t.idtype == type && GetFullName(t) == name)
		{
			return t.color;
		}
	}
	return 'white';

}
function EnableByName(name, type)
{
	for(var i = 0; i < TimerArray.length; ++i)
	{
		var t = TimerArray[i];
		if(t.idtype == type && GetFullName(t) == name)
		{
			WSSendMessage("c" + t.id);
		}
	}
}


function OnLoadPreset(NewSettings, Apply, RO)
{
	if(Apply)
	{
		WSSendMessage("d");
		PresetPending = 0;
	}
	ActivePreset = NewSettings.PresetName;
	ActivePresetRO = RO ? 1 : 0;
	console.log('loading preset ' + ActivePresetRO);
	var EnableCount = 0;
	for(var idx in NewSettings)
	{
		if(idx == 'Timers')
		{
			if(Apply)
			{
				var Timers = NewSettings[idx];
				for(var i = 0; i < Timers.length; ++i)
				{
					var FullName = Timers[i];
					EnableCount++;
					EnableByName(FullName, TYPE_TIMER);
				}
			}
		}
		else if(idx == 'Groups')
		{
			if(Apply)
			{
				var Groups = NewSettings[idx];
				for(var i = 0; i < Groups.length; ++i)
				{
					var FullName = Groups[i];
					EnableByName(FullName, TYPE_GROUP);

				}
			}
		}
		else
		{
			Settings[idx] = NewSettings[idx];
		}
	}

	if(Settings.FunctionsInstrumented && Apply)
	{
		if(Settings.FunctionsInstrumented.length == Settings.FunctionsInstrumentedModule.length && Settings.FunctionsInstrumented.length == Settings.FunctionsInstrumentedUnmangled.length)
		{
			var Msg = "D" + Settings.FunctionsInstrumented.length + " ";
			for(var i = 0; i < Settings.FunctionsInstrumented.length; ++i)
			{
				Msg += Settings.FunctionsInstrumentedModule[i] + "!" + Settings.FunctionsInstrumentedUnmangled[i] + "!";
			}
			WSSendMessage(Msg);
		}
	}
	if(Settings.ViewActive >= 0)
	{
		ResizeCanvas();
	}
}
function SanitizeString(s)
{
	let r = "";
	for(let i = 0; i < s.length; ++i)
	{
		let c = s[i];
		if(!((c>='a' && c<='z') || (c >='A' && c <= 'Z') || (c >= '0' && c <= '9')))
		{
			r += '_';
		}
		else
		{
			r += c;
		}
	}
	return r;
}
function LoadPreset(Name, RO)
{
	Name = SanitizeString(Name);
	WSSendMessage((RO?"m":"l")+Name);
}

function SavePreset(Name)
{
	Name = SanitizeString(Name);
	AddPreset(Name);
	var Timers = [];
	var Groups = [];
	for(var i = 0; i < TimerArray.length; ++i)
	{
		var t = TimerArray[i];
		var idtype = TimerArray[i].idtype;
		if(t.e)
		{
			if(idtype == TYPE_TIMER)
			{
				Timers.push(GetFullName(t));
			}
			else if(idtype == TYPE_GROUP)
			{
				Groups.push(GetFullName(t));
			}
		}
	}

	Settings.Timers = Timers;
	Settings.Groups = Groups;
	Settings.PresetName = Name;
	let Clone = function(A)
	{
		let N = Array(A.length);
		for(var i = 0; i < A.length; ++i)
		{
			N[i] = A[i];
		}
		return N;
	};
	Settings.FunctionsInstrumented = Clone(FunctionsInstrumented);
	Settings.FunctionsInstrumentedModule = Clone(FunctionsInstrumentedModule);
	Settings.FunctionsInstrumentedUnmangled = Clone(FunctionsInstrumentedUnmangled);

	var JsonSettings = JSON.stringify(Settings);
	console.log('settings stored ' + JsonSettings);
	WSSendMessage("s"+Name+","+JsonSettings);
	ActivePreset = Name;
	ActivePresetRO = 0;
}
function DrawMenuPresets()
{
	var Selection = null;
	var SizeInfo = {};
	SizeInfo.h = BoxHeight * (Presets.length * 2);
	if(ReadOnlyPresets.length)
	{
		SizeInfo.h += BoxHeight * (Presets.length + 1);
	}
	var x = MenuItems[SubMenuPresets].x;
	var y = MenuItems[SubMenuPresets].y;

	var Width = 50;
	var WLeft = MeasureArray(0, ["Save ", "Load ", "Save As ..", "Builtin" ]);
	Width = MeasureArray(Width, Presets);
	var Width = 35 + Width + WLeft;
	SizeInfo.w = Width;
	SizeInfo.x = x;
	SizeInfo.y = y;
	
	var M = CreateMenuState(SizeInfo);
	var context = CanvasDetailedView.getContext('2d');

	var SettingsCached;
	var SettingsCachedY = 0;

	for(var i = 0; i < Presets.length; ++i)
	{
		var Active = (ActivePresetRO == 0 && ActivePreset == Presets[i]) ? 1 : 0;
		if(DrawMenuElementMouseIn(M))
		{
			SettingsCachedY = M.y;
			SettingsCached = PresetsCache[Presets[i]];
		}

		if(DrawMenuElement(M, Active, "Load", Presets[i], 'white', 0))
		{
			LoadPreset(Presets[i]);
		}
	}
	DrawMenuElement(M, 0, "---", "", 'white', 0);
	if(DrawMenuElement(M, 0, "Save As..", "", 'white', 0))
	{
		var str = ShowPrompt('Enter Preset Name', '');
		if(str.length>1)
		{
			SavePreset(str);
		}

	}

	for(var i = 0; i < Presets.length; ++i)
	{
		var Active = (ActivePresetRO == 0 && ActivePreset == Presets[i]) ? 1 : 0;
		if(DrawMenuElement(M, Active, "Save", Presets[i], 'white', 0))
		{
			SavePreset(Presets[i]);
		}
	}
	if(ReadOnlyPresets && ReadOnlyPresets.length > 0)
	{
		DrawMenuElement(M, 0, "---", "", 'white', 0);
		for(var i = 0; i < ReadOnlyPresets.length; ++i)
		{
			var Active = (ActivePresetRO && ActivePreset == ReadOnlyPresets[i]) ? 1 : 0;
			if(DrawMenuElementMouseIn(M))
			{
				SettingsCachedY = M.y;
				SettingsCached = ReadOnlyPresetsCache[ReadOnlyPresets[i]];
			}
			if(DrawMenuElement(M, Active, "Builtin", ReadOnlyPresets[i], 'white', 0))
			{
				LoadPreset(ReadOnlyPresets[i], 1);
			}
		}
	}
	SizeInfo.h = M.y - SizeInfo.y;


	if(SettingsCached)
	{
		var SizeLeft = 10 * FontWidth;
		var Timers = SettingsCached.Timers;
		var Groups = SettingsCached.Groups;
		var Patched = SettingsCached.Patched;
		var W = 0;
		var H = 0;
		if(Timers)
		{
			W = MeasureArray(W, Timers);
			H = 1 + Timers.length;
		}
		if(Groups)
		{
			W = MeasureArray(W, Groups);
			H = 1 + Groups.length;
		}
		if(Patched)
		{
			W = MeasureArray(W, Patched);
			H = 1 + Patched.length;
		}
		W += 40;
		H = H * (FontHeight+1);
		var M = CreateMenuState(SizeInfo);
		M.x += M.w + 5;
		M.y = SettingsCachedY;
		M.w = W;
		M.h = H;

		if(Groups && Groups.length)
		{
			DrawMenuElement(M, 0, "Groups", "", 'white', 0);
			for(var i = 0; i < Groups.length; ++i)
			{
				DrawMenuElement(M, 0, "", Groups[i], 'white', 0);
			}
		}
		if(Timers && Timers.length)
		{
			DrawMenuElement(M, 0, "Timers", "", 'white', 0);
			for(var i = 0; i < Timers.length; ++i)
			{
				DrawMenuElement(M, 0, "", Timers[i], ColorFromFullName(Timers[i], TYPE_TIMER), 0);
			}
		}
		if(Patched && Patched.length)
		{
			DrawMenuElement(M, 0, "Patched", "", 'white', 0);
			for(var i = 0; i < Patched.length; ++i)
			{
				DrawMenuElement(M, 0, "", Patched[i], 'white', 0);
			}
		}


	}

	return SizeInfo;
}

function DrawMenuColumns()
{
	var Selection = null;
	var SizeInfo = {};
	var BarColumnNames = GetBarColumnNames();
	var BarColumnEnabled = GetBarColumnEnabled();
	SizeInfo.h = BoxHeight * (BarColumnNames.length);
	var x = MenuItems[SubMenuColumns].x;
	var y = MenuItems[SubMenuColumns].y;

	var Width = MeasureArray(0, BarColumnNames);
	SizeInfo.w = Width;
	SizeInfo.x = x;
	SizeInfo.y = y;
	
	var M = CreateMenuState(SizeInfo);
	var context = CanvasDetailedView.getContext('2d');
	context.fillRect(M.x, M.y, Width, SizeInfo.h);


	for(var i = 0; i < BarColumnNames.length; ++i)
	{
		var Active = ActivePreset == Presets[i] ? 1 : 0;
		if(DrawMenuElement(M, BarColumnEnabled[i], BarColumnNames[i], "", 'white', 0))
		{
			BarColumnEnabled[i] = !BarColumnEnabled[i];
			for(var j = 0; j < ColumnsWidth.length; ++j)
			{
				ColumnsWidth[i] = 20;
			}
		}
	}

	SizeInfo.h = M.y - SizeInfo.y;
	return SizeInfo;

}


function ToggleView()
{
	Settings.ViewActive = (Settings.ViewActive+1) % VIEW_SIZE;
	ActivateView(Settings.ViewActive);
}

function ActivateView(idx)
{
	for(var i = 1; i < Views.length; ++i)
	{
		Views[i].visible = false;
	}
	if(idx == VIEW_GRAPH_SPLIT)
	{
		MainView.DisplayFunc = DrawGraphSplit;
		MainView.visible = true;
	}
	else if(idx == VIEW_GRAPH_PERCENTILE)
	{
		MainView.DisplayFunc = DrawGraphPercentile;
		MainView.visible = true;
	}
	else if(idx == VIEW_GRAPH_THREAD_GROUP)
	{
		MainView.DisplayFunc = DrawGraphThreadGroup;
		MainView.visible = true;
	}
	else if(idx == VIEW_BAR)
	{
		MainView.DisplayFunc = DrawTableView;
		MainView.visible = true;
	}
	else if(idx == VIEW_COUNTERS)
	{
		MainView.DisplayFunc = DrawCounterView;
		MainView.visible = true;
	}
	else if(idx == VIEW_BAR_SINGLE || idx == VIEW_BAR_ALL)
	{
		for(var i = 0; i < X7Views.length; ++i)
		{
			X7Views[i].visible = true;
		}
		SingleTimerBars = idx == VIEW_BAR_SINGLE;
	}
	Settings.ViewActive = idx;
	X7BarColumnMask = -1;
	ViewBarMaxMsTextLength  = 0;
	var hest = 3;
}
function DrawMenuViews()
{
	var ViewClick = function(idx, name)
	{
		ActivateView(idx);
	};
	var x = MenuItems[SubMenuViews].x;
	var y = MenuItems[SubMenuViews].y;
	return DrawMenuGeneric(ViewNames, Settings.ViewActive, ViewClick, x, y, ViewNames2);
}

function TweakValue(Value, Tweak, amount, Min, Max)
{
	var V = 0.98;
	if(Tweak<0)
	{
		for(var x = 0; x < Math.abs(Tweak); ++x)
		{
			var newValue = Math.floor(Value*V);
			if(newValue == Value)
			{
				Value--;
			}
			else
			{
				Value = newValue;
			}
		}
	}
	else
	{
		var RcpV = 1.0 / V;

		for(var x = 0; x < Tweak; ++x)
		{
			var newValue = Math.ceil(Value*RcpV);
			if(newValue == Value)
			{
				Value++;
			}
			else
			{
				Value = newValue;
			}
		}
	}
	if(Min!=null && Value < Min)
		Value = Min;
	if(Max!=null && Value > Max)
		Value = Max;
	return Value;

}

function NextValue(Presets, Value, Dir)
{
	var idx = 0;	
	if(Dir > 0)
	{
		for(idx = 0;idx < Presets.length; ++idx)
		{
			if(Presets[idx] > Value)
			{
				break;
			}
		}
	}
	else
	{
		for(idx = Presets.length; idx >= 0; idx -= 1)
		{
			if(Presets[idx] < Value)
			{
				break;
			}
		}
	}
	idx = (idx + Presets.length) % Presets.length;
	return Presets[idx];
}
function AutoCaptureRoll(Direction, Tweak, SetDirect)
{
	if(SetDirect)
	{
		Settings.AutoCaptureTheshold = SetDirect;
	}
	else if(Tweak)
	{
		Settings.AutoCaptureTheshold = TweakValue(Settings.AutoCaptureTheshold, Tweak, 0.98, 1, 500);
	}
	else if(Direction)
	{
		Settings.AutoCaptureTheshold = NextValue(AutoCaptureThesholdPresets, Settings.AutoCaptureTheshold, Direction);
	}
}
function AutoCaptureRepeatRoll(Direction, Tweak, SetDirect)
{
	if(SetDirect)
	{
		Settings.AutoCaptureRepeat = SetDirect;
	}
	else if(Tweak)
	{
		Settings.AutoCaptureRepeat = TweakValue(Settings.AutoCaptureRepeat, Tweak, 0.98, 1, 500);
	}
	else if(Direction)
	{
		Settings.AutoCaptureRepeat = NextValue(AutoCaptureRepeatPresets, Settings.AutoCaptureRepeat, Direction);
	}
}
function CaptureRoll(Direction, Tweak, SetDirect)
{
	if(SetDirect)
	{
		Settings.CaptureFrames = SetDirect;
	}
	else if(Tweak)
	{
		Settings.CaptureFrames = TweakValue(Settings.CaptureFrames, Tweak, 0.98, 5, 300);

	}
	else if(Direction)
	{
		Settings.CaptureFrames = NextValue(CaptureFramesPresets, Settings.CaptureFrames, Direction);
	}
}
function CaptureDelayRoll(Direction, Tweak, SetDirect)
{
	if(SetDirect)
	{
		Settings.CaptureDelay = SetDirect;
	}
	else if(Tweak)
	{
		Settings.CaptureDelay = TweakValue(Settings.CaptureDelay, Tweak, 0.98, 5, 300);

	}
	else if(Direction)
	{
		Settings.CaptureDelay = NextValue(CaptureDelayPresets, Settings.CaptureDelay, Direction);
	}
}

function GetAutoCaptureString()
{
	if(AutoCaptureSourceIndex >= 0)
	{
		if(AutoCaptureSourceIndex >= EnabledArray.length)
		{
			AutoCaptureSourceIndex = -1;
			AutoCaptureSourceIndex = -1;
		}
		else
		{
			var idx = EnabledArray[AutoCaptureSourceIndex];
			return TimerArray[idx].name;
		}
	}
	return "Frame Time";
}

function AutoCaptureSourceRoll(Direction, Tweak, SetDirect)
{
	if(SetDirect)
	{

	}else if(Tweak||Direction)
	{
		if(!Direction)
			Direction = Tweak;
		if(Direction<0)
		{
			AutoCaptureSourceIndex--;
			if(AutoCaptureSourceIndex<-1)
			{
				AutoCaptureSourceIndex = EnabledArray.length-1;
			}
		}
		else
		{
			AutoCaptureSourceIndex++;
			if(AutoCaptureSourceIndex >= EnabledArray.length)
			{
				AutoCaptureSourceIndex = -1;
			}
		}

	}
}

function ShowPrompt(Title, Value, Type)
{
	var v;
	if(Type == 'int')
	{
		var newValue = prompt('' + Title, '' + Value);
		v = parseInt(newValue);
	}
	else if(Type == 'float')
	{
		var newValue = prompt('' + Title, '' + Value);
		v = parseFloat(newValue);
	}
	else
	{
		var v = prompt('' + Title, '' + Value);
		return v;
	}
	if(isNaN(v))
	{
		return Value;
	}
	else
	{
		return v;
	}

}
function MeasureArray(v, A, f)
{
	var context = CanvasDetailedView.getContext('2d');	
	for(var i = 0; i < A.length; ++i)
	{
		var s = A[i];
		if(f)
			s=f(s, i);
		var l = context.measureText(s).width;
		v = v < l ? l : v;
	}
	return v;
}

function CreateMenuState(SizeInfo)
{
	var MenuState = {};
	for(var i in SizeInfo)
	{
		MenuState[i] = SizeInfo[i];
	}
	MenuState.cidx = 0;
	return MenuState;
}
function DrawMenuElementMouseIn(M)
{
	return MouseY >= M.y && MouseY < M.y + BoxHeight;
}
function DrawMenuElement(M, Selected, Name, Value, color)
{
	var context = CanvasDetailedView.getContext('2d');

	var bMouseIn = DrawMenuElementMouseIn(M);
	var YText = M.y + BoxHeight - FontAscent;

	var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[M.cidx];
	context.fillStyle = Selected?'white':bgcolor;
	context.fillRect(M.x-2, M.y, M.w + 4, BoxHeight);
	context.fillStyle = bgcolor;
	context.fillRect(M.x, M.y, M.w, BoxHeight);
	context.fillStyle = color;
	context.fillText(Name, M.x, YText);
	context.textAlign = 'right';
	context.fillText('' + Value, M.x + M.w - 2, YText);
	context.textAlign = 'left';
	M.cidx = 1-M.cidx;
	M.y += BoxHeight;
	return bMouseIn && MouseReleased;
}
function DrawMenuRoll(M, Name, RollValue, RollExt, RollFunction, Tweak, Type)
{
	var context = CanvasDetailedView.getContext('2d');

	var YText = M.y + BoxHeight - FontAscent;
	var SizeMinus = context.measureText('-').width;
	var SizePlus = context.measureText('+').width;

	var bMouseIn = MouseY >= M.y && MouseY < M.y + BoxHeight;
	var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[M.cidx];
	context.fillStyle = bgcolor;
	context.fillRect(M.x-2, M.y, M.w+4, BoxHeight);
	context.fillStyle = 'white';
	context.fillText(Name, M.x, YText);
	context.textAlign = 'right';
	var XTemp = M.x + M.w - 3;

	if(KeyShiftDown && bMouseIn)
	{
		if(Tweak>=0)
		{
			RollFunction(0, MouseX-Tweak);
		}
		Tweak = MouseX;
		bMouseIn = false;
	}
	else
	{
		Tweak = -1;
	}

	var MouseReleasedUsed = false;
	if(bMouseIn && MouseX >= XTemp - SizePlus && MouseX <= XTemp)
	{
		context.fillStyle = 'red';
		if(MouseReleased)
		{
			RollFunction(1);
			MouseReleasedUsed = true;
		}
	}
	else
	{
		context.fillStyle = 'white';
	}
	context.fillText('+', XTemp, YText);
	XTemp -= SizePlus + 3;



	if(bMouseIn && MouseX >= XTemp - SizeMinus && MouseX <= XTemp)
	{
		context.fillStyle = 'red';
		if(MouseReleased)
		{
			RollFunction(-1);
			MouseReleasedUsed = true;				
		}
	}
	else
	{
		context.fillStyle = 'white';
	}

	context.fillText('-', XTemp, YText);
	XTemp -= SizeMinus + 3;
	var RollText = ''+RollValue + RollExt;
	var RollWidth = context.measureText(RollText).width;
	context.fillStyle = 'white';

	context.fillText(RollText, XTemp, YText);
	XTemp -= RollWidth;
	
	context.textAlign = 'left';

	M.cidx = 1-M.cidx;

	M.y += BoxHeight;
	if(Type && MouseReleased && bMouseIn && !MouseReleasedUsed)
	{
		var V = ShowPrompt(Name, RollValue, Type);
		RollFunction(0,0,V);
	}
	return Tweak;

}
function CheckPopupAllowed()
{
	if(!PopupsAllowed && !PopupTestPending)
	{
		PopupTestPending = 1;
		PopupsAllowed = 0;
		PopupsFailed = 0;
		let DoPopupTest = function()
		{
			var W = window.open("about:blank");
			var Fail = function()
			{
				PopupTestPending = 0;
				PopupsFailed = 1;
				PopupsAllowed = 0;
			};
			var Check = function()
			{
				if(!(W.innerHeight>0))
					Fail();
				else
				{
					PopupTestPending = 0;
					PopupsFailed = 0;
					PopupsAllowed = 1;
					W.close();
				}
			};
			if(W)
			{
				if(/chrome/.test(navigator.userAgent.toLowerCase()))
					setTimeout(Check, 200);
				else
					W.onload = Check;
			}
			else
			{
				Fail();
			}
		};
		setTimeout(DoPopupTest, 5000);
	}
}

function GetSubGraphReferenceTime(SubGraphSettings, TimerState)
{
	if(SubGraphSettings.AutomaticReference)
	{
	 	return TimerState.historymaxsoft ? TimerState.historymax : TimerState.historymax;
	}
	else
	{
		return SubGraphSettings.ReferenceTime;
	}
}
function GetSubGraphSettingsKey(key)
{
	if(Settings.ViewActive == VIEW_GRAPH_PERCENTILE)
	{
		return "perc_"+key;
	}
	else
	{
		return key;
	}
}
function GetSubGraphSettings(key)
{
	let k = GetSubGraphSettingsKey(key);
	let SubGraphSettings = Settings.SubGraphSettings[k];
	if(!SubGraphSettings)
	{
		SubGraphSettings = {"ReferenceTime":10.0, "TargetTime":-1, "AutomaticReference":1, "Percentile":0.0};
		Settings.SubGraphSettings[k] = SubGraphSettings;
	}
	return SubGraphSettings;
}


function DrawMenuGraphSettings()
{
	let Percentile = Settings.ViewActive == VIEW_GRAPH_PERCENTILE;
	let Selection = null;
	let SizeInfo = {};
	SizeInfo.h = (Percentile?3:2) * BoxHeight;
	let Strings = ["AutoCapture Enabled", "AutoCapture Threshold", "AutoCapture Source", "Capture Length"];
	let wLeft = MeasureArray(0, Strings);
	let wRight = 50;
	wRight = MeasureArray(wRight, ["Frame Time"]);
	wRight = MeasureArray(wRight, EnabledArray, function(a){return TimerArray[a].name; } );
	let Width = wLeft + 35 + wRight;
	SizeInfo.w = Width;
	SizeInfo.x = CaptureButtonX;
	SizeInfo.y = CaptureButtonY;
	
	let M = CreateMenuState(SizeInfo);
	let context = CanvasDetailedView.getContext('2d');
	context.fillRect(M.x, M.y, Width, SizeInfo.h);
	let SubGraphSettings = GetSubGraphSettings(SubMenuGraphSettingsKey);


	ReferenceTimeTweak = DrawMenuRoll(M, "Reference Time", SubGraphSettings.ReferenceTime, '', ReferenceRollSubGraph, ReferenceTimeTweak, 'int');
	if(DrawMenuElement(M, SubGraphSettings.AutomaticReference, "Automatic Reference Time", SubGraphSettings.AutomaticReference, 'white'))
	{
		SubGraphSettings.AutomaticReference = 1-SubGraphSettings.AutomaticReference;
	}
	if(Percentile)
	{
		//PercentileTweak = DrawMenuRoll(M, "Percentile", SubGraphSettings.Percentile, '', PercentileRollSubGraph, PercentileTweak, 'int');
		if(DrawMenuElement(M, 0, "Clear Aggregate", "", 'white'))
		{
			let TimerMap = FrameData.TimerMap;
			if(TimerMap)
			{
				let TimerState = TimerMap[SubMenuGraphSettingsKey];
				TimerState.PercentileMax = -1e38;
				TimerState.PercentileMin = 1e38;
				TimerState.Percentile = new Float32Array(PERCENTILE_SAMPLES);
				TimerState.Percentile.fill(0.0);
				TimerState.PercentileCount = 0;
			}
		}
	}
	SizeInfo.h = M.y - SizeInfo.y;
	return SizeInfo;

}

function Dots()
{
	let t = new Date().getMilliseconds() / 200;
	let dots = [".  ",".. ","...",".. "];
	let x = Math.floor(t) %4;
	return dots[x];
}
function DrawMenuCapture()
{
	let Selection = null;
	let SizeInfo = {};
	SizeInfo.h = 7 * BoxHeight;
	let Strings = ["Popups Allowed", "AutoCapture Enabled", "AutoCapture Threshold", "AutoCapture Source", "Capture Length", "Capture Delay"];
	let wLeft = MeasureArray(0, Strings);
	let wRight = 50;
	wRight = MeasureArray(wRight, ["Frame Time"]);
	wRight = MeasureArray(wRight, EnabledArray, function(a){return TimerArray[a].name; } );
	let Width = wLeft + 35 + wRight;
	SizeInfo.w = Width;
	SizeInfo.x = CaptureButtonX - SizeInfo.w;
	SizeInfo.y = CaptureButtonY - SizeInfo.h;
	
	let M = CreateMenuState(SizeInfo);
	let context = CanvasDetailedView.getContext('2d');
	context.fillRect(M.x, M.y, Width, SizeInfo.h);
	let d = Dots();
	let PopupStatus = PopupsAllowed ? "Yes" :  (PopupsFailed ? "No" : (PopupTestPending ? ("Testing"+d) : "Untested"));
	if(DrawMenuElement(M, 0, "Popups Allowed", PopupStatus, 'white', 0))
	{
		CheckPopupAllowed();
	}
	let AutoStatus = AutoCaptureEnabled > 0 ? "on" : "off";
	if(CaptureTriggerTime != null && CaptureTriggerTimeType == 2)
	{
		AutoStatus = "pending"+d;
	}
	if(DrawMenuElement(M, 0, "AutoCapture Enabled", AutoStatus, 'white', 0))
	{
		if(!AutoCaptureEnabled && CaptureTriggerTime == null)
		{
			if(Settings.CaptureDelay <= 0)
			{
				AutoCaptureEnabled = Settings.AutoCaptureRepeat > 0 ? Settings.AutoCaptureRepeat : 1;
			}
			else
			{
				CaptureTriggerTime = new Date();
				CaptureTriggerTimeType = 2; 
			}
		}
		else
		{
			AutoCaptureEnabled = 0;
			if(CaptureTriggerTimeType == 2)
			{
				CaptureTriggerTimeType = 0;
				CaptureTriggerTime = null;
			}
		}
	}
	AutoCaptureSourceTweak = DrawMenuRoll(M, "AutoCapture Source", GetAutoCaptureString(), '', AutoCaptureSourceRoll, AutoCaptureSourceTweak);
	AutoCaptureTweak = DrawMenuRoll(M, "AutoCapture Threshold", Settings.AutoCaptureTheshold, 'ms', AutoCaptureRoll, AutoCaptureTweak, 'int');
	AutoCaptureRepeatTweak = DrawMenuRoll(M, "AutoCapture Repeat", Settings.AutoCaptureRepeat, '', AutoCaptureRepeatRoll, AutoCaptureRepeatTweak, 'int');
	CaptureTweak = DrawMenuRoll(M, "Capture Length",  Settings.CaptureFrames, '', CaptureRoll, CaptureTweak, 'int');
	CaptureDelayTweak = DrawMenuRoll(M, "Capture Delay",  Settings.CaptureDelay, '', CaptureDelayRoll, CaptureDelayTweak, 'int');

	SizeInfo.h = M.y - SizeInfo.y;
	return SizeInfo;

}
function WindowRect(x,y,w,h)
{
	let s = {};
	s.x = x;
	s.y = y;
	s.w = w;
	s.h = h;
	return s;
}
function GetAggregateString()
{
	if(0 == Settings.AggregateFrames)
		return 'infinite';
	else
		return Settings.AggregateFrames + '';
}

function AggregateRoll(Direction, Tweak, SetDirect)
{
	if(SetDirect || SetDirect == 0)
	{
		if(SetDirect <= 0 || SetDirect == 'infinite')
		{
			Settings.AggregateFrames = 0;
		}
		else
		{
			Settings.AggregateFrames = SetDirect;
		}
	}
	else if(Tweak)
	{
		Settings.AggregateFrames = TweakValue(Settings.AggregateFrames, Tweak, 0.98, 0, 500);

	}
	else if(Direction)
	{
		Settings.AggregateFrames = NextValue(AggregatePresets, Settings.AggregateFrames, Direction);
	}
}
function ReferenceRoll(Direction, Tweak, SetDirect)
{
	if(SetDirect)
	{
		Settings.ReferenceTime = SetDirect;
	}
	else if(Tweak)
	{
		Settings.ReferenceTime = TweakValue(Settings.ReferenceTime, Tweak, 0.98, 5, 1000);

	}
	else if(Direction)
	{
		Settings.ReferenceTime = NextValue(ReferencePresets, Settings.ReferenceTime, Direction);
	}
}

function ReferenceRollSubGraph(Direction, Tweak, SetDirect)
{
	let SubGraphSettings = GetSubGraphSettings(SubMenuGraphSettingsKey);
	if(SetDirect)
	{
		SubGraphSettings.ReferenceTime = SetDirect;
	}
	else if(Tweak)
	{
		SubGraphSettings.ReferenceTime = TweakValue(SubGraphSettings.ReferenceTime, Tweak, 0.98, 5, 1000);

	}
	else if(Direction)
	{
		SubGraphSettings.ReferenceTime = NextValue(ReferencePresets, SubGraphSettings.ReferenceTime, Direction);
	}
}

function PercentileRollSubGraph(Direction, Tweak, SetDirect)
{
	let SubGraphSettings = GetSubGraphSettings(SubMenuGraphSettingsKey);
	if(SetDirect)
	{
		SubGraphSettings.Percentile = SetDirect;
	}
	else if(Tweak)
	{
		SubGraphSettings.Percentile = TweakValue(SubGraphSettings.Percentile, Tweak, 0.98, 5, 1000);
	}
	else if(Direction)
	{
		SubGraphSettings.Percentile = NextValue(PercentilePresets, SubGraphSettings.Percentile, Direction);
	}
}


function TargetRoll(Direction, Tweak, SetDirect)
{
	if(SetDirect)
	{
		Settings.TargetTime = SetDirect;
	}
	else if(Tweak)
	{
		Settings.TargetTime = TweakValue(Settings.TargetTime, Tweak, 0.98, 5, 1000);

	}
	else if(Direction)
	{
		Settings.TargetTime = NextValue(ReferencePresets, Settings.TargetTime, Direction);
	}
}
function TargetRollSubGraph(Direction, Tweak, SetDirect)
{
	let SubGraphSettings = GetSubGraphSettings(SubMenuGraphSettingsKey);
	if(SetDirect)
	{
		SubGraphSettings.TargetTime = SetDirect;
	}
	else if(Tweak)
	{
		SubGraphSettings.TargetTime = TweakValue(SubGraphSettings.TargetTime, Tweak, 0.98, 5, 1000);

	}
	else if(Direction)
	{
		SubGraphSettings.TargetTime = NextValue(ReferencePresets, SubGraphSettings.TargetTime, Direction);
	}
}
function DrawMenuSettings()
{
	var context = CanvasDetailedView.getContext('2d');
	var nColorIndex = 0;
	var SizeInfo = AggregateMenuSize();
	SizeInfo.x = MenuItems[SubMenuSettings].x;
	SizeInfo.y = MenuItems[SubMenuSettings].y;

	SizeInfo.w = 200;
	var M = CreateMenuState(SizeInfo);

	AggregateTweak = DrawMenuRoll(M, "Aggregate Frames", GetAggregateString(), '', AggregateRoll, AggregateTweak, 'int');
	ReferenceTimeTweak = DrawMenuRoll(M, "Reference Time", Settings.ReferenceTime, '', ReferenceRoll, ReferenceTimeTweak, 'int');
	TargetTimeTweak = DrawMenuRoll(M, "Target Time", Settings.TargetTime, '', TargetRoll, TargetTimeTweak, 'int');
	if(DrawMenuElement(M, Settings.AutomaticReference, "Automatic Reference Time", Settings.AutomaticReference, 'white'))
	{
		Settings.AutomaticReference = 1-Settings.AutomaticReference;
	}
	if(DrawMenuElement(M, Settings.ViewCompressed, "Compressed View", Settings.ViewCompressed, 'white'))
	{
		Settings.ViewCompressed = 1-Settings.ViewCompressed;
		ResizeCanvas();
	}

	if(Settings.AggregateFrames <= 0)
	{
		if(DrawMenuElement(M, 0, "Clear Aggregate", "Current[" + AggregateCurrent + "]", 'white'))
		{
			WSSendMessage("r");
		}
	}
	if(DrawMenuElement(M, 0, "Clear Percentile Aggregate", "", 'white'))
	{
		let TimerMap = FrameData.TimerMap;
		if(TimerMap)
		{
			for(let key in TimerMap)
			{
				let idx = GetTimer(key);
				let TimerState = TimerMap[key];
				if(!IsGroup(key) && TimerState.Percentile)
				{
					TimerState.PercentileMax = -1e38;
					TimerState.PercentileMin = 1e38;
					TimerState.Percentile = new Float32Array(PERCENTILE_SAMPLES);
					TimerState.Percentile.fill(0.0);
					TimerState.PercentileCount = 0;
				}
			}
		}
	}


	if(DrawMenuElement(M, Settings.AllowHighDPI, "Allow High DPI", Settings.AllowHighDPI, 'white'))
	{
		Settings.AllowHighDPI = 1 - Settings.AllowHighDPI;
		ResizeCanvas();
	}
	if(DrawMenuElement(M, Cookie.CodeReportMode == 0, "Code Report: Prompt", Cookie.CodeReportMode == 0 ? 1 : 0, 'white'))
	{
		Cookie.CodeReportMode = 0;
		WriteCookie();
	}

	if(DrawMenuElement(M, Cookie.CodeReportMode == 1, "Code Report: Report Silently", Cookie.CodeReportMode == 1 ? 1 : 0, 'white'))
	{
		Cookie.CodeReportMode = 1;
		WriteCookie();
	}
	if(DrawMenuElement(M, Cookie.CodeReportMode == 2, "Code Report: Never Report", Cookie.CodeReportMode == 2 ? 1 : 0, 'white'))
	{
		Cookie.CodeReportMode = 2;
		WriteCookie();
	}

	SizeInfo.h = M.y - SizeInfo.y;
	return SizeInfo;
}

function MoveFilterInputDiv(x, y, w)
{
	if(FilterInputDivPos.x != x || FilterInputDivPos.y != y || FilterInputDivPos.w != w)
	{
		FilterInputDivPos.x = x;
		FilterInputDivPos.y = y;
		FilterInputDivPos.w = w;
		FilterInputDiv.style['left'] = x + 'px';
		FilterInputDiv.style['top'] = y + 'px';
		FilterInput.style['width'] = w + 'px';
	}
}

function DrawMenuTimer()
{
	if(FilterInputValueLast != FilterInput.value)
	{
		nOffsetMenuTimers = 0;
	}
	FilterInputValueLast = FilterInput.value;
	var FilterArray = CreateFilter(FilterInput.value);
	var context = CanvasDetailedView.getContext('2d');
	var nColorIndex = 0;
	var SizeInfo = TimerMenuSize();
	SizeInfo.x = MenuItems[SubMenuTimers].x;
	SizeInfo.y = MenuItems[SubMenuTimers].y;
	var Y = SizeInfo.y;
	var Width = SizeInfo.w;
	var Selection = null;
	var X = SizeInfo.x;
	MoveFilterInputDiv(SizeInfo.x, SizeInfo.y, SizeInfo.w);
	Y += 35;

	var bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight;
	var bMouseInClear =  MouseY >= Y && MouseY < Y + BoxHeight;
	var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
	var TextY = Y+BoxHeight-FontAscent;
	var YClear = Y;
	var TextYClear = TextY;
	var YStart = Y;

	var MouseTaken = bMouseIn;

	Y += BoxHeight;
	nColorIndex = 1-nColorIndex;

	Y -= nOffsetMenuTimers;

	for(var i = 0; i < TimerArray.length; ++i)
	{
		var v = TimerArray[i];
		if(v.idtype == TYPE_TIMER
		 //|| v.idtype == TYPE_GROUP
		 )
		{
			var Name = v.name;
			var ParentName = TimerArray[v.parent].name;
			if(FilterMatch(FilterArray, ParentName + " " + Name) && (0 == TimersActiveOnly || v.e))
			{
				if(Y > YStart)
				{
					var ParentColor = TimerArray[v.parent].e ? 'white' : 'grey';
					bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight && !MouseTaken;
					bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
					TextY = Y+BoxHeight-FontAscent;
					context.fillStyle = v.e?'white':bgcolor;
					context.fillRect(X-2, Y, Width+4, BoxHeight);
					context.fillStyle = bgcolor;
					context.fillRect(X, Y, Width, BoxHeight);
					context.fillStyle = ParentColor;
					context.fillText(ParentName, X + 2, TextY);
					context.fillStyle = v.color;
					context.textAlign = 'right';
					context.fillText(Name, X + Width - 2, TextY);
					context.textAlign = 'left';
					if(bMouseIn)
					{
						Selection = v.id;
					}
				}
				Y += BoxHeight;
				nColorIndex = 1-nColorIndex;
			}
		}
	}

	{

		var c0 = nBackColorOffset;
		var c1 = nBackColors[1];
		var W = Width + 4;
		var WHalf = W / 2;
		var InsideClear = bMouseInClear && MouseX-X< WHalf-1;
		var InsideActive =  bMouseInClear && MouseX-X > WHalf+1;
		context.fillStyle = nBackColors[0];
		context.fillRect(X-2, YClear, W, BoxHeight);

		context.fillStyle = InsideClear ? c0 :c1;
		context.fillRect(X-2, YClear, WHalf-1, BoxHeight);
		context.fillStyle = InsideActive ? c0 :c1;
		context.fillRect(X-2 + WHalf + 1, YClear, WHalf-1, BoxHeight);


		context.fillStyle = 'white';
		context.textAlign = 'center';
		context.fillText("[clear] ", X + 2 + Width * 0.25, TextYClear);
		context.fillStyle = TimersActiveOnly ? 'white' : 'grey';
		context.fillText("[active only]", X + 2 + Width * 0.75, TextYClear);
		if(MouseReleased)
		{
			if(InsideActive)
			{
				TimersActiveOnly = 1-TimersActiveOnly;
			}
			if(InsideClear)
			{
				WSSendMessage("x");
			}
		}
	}
	context.textAlign = 'left';
	if(Selection && MouseReleased && !MouseTaken)
	{
		WSSendMessage("c" + Selection);
	}
	SizeInfo.h = Y-SizeInfo.y;
	return SizeInfo;
}

var LoadSymbolAnim = -1;
function LoadSymbolAnimRestart()
{
	if(LoadSymbolAnim > 0)
	{
		LoadSymbolAnim += 1;
	}
	else
	{
		LoadSymbolAnim = 1;
	}
}
function LoadSymbolAnimUpdateAndGet()
{
	if(LoadSymbolAnim>=0)
	{
		LoadSymbolAnim -= 0.03;
		if(LoadSymbolAnim < 0)
		{
			LoadSymbolAnim = -1;
		}
		else
		{
			return Math.sin(3.14  * LoadSymbolAnim);
		}

	}
	return -1;
}


var CircleSteps = 30;
var CircleX = Array(CircleSteps);
var CircleY = Array(CircleSteps);
function InitCircle()
{
	var twopi = 2 * Math.PI;
	for(var i = 0; i < CircleSteps; ++i)
	{
		var s = (1.0*i) / CircleSteps;

		var f = (1.0 * s) * twopi;
		CircleX[i] = Math.sin(f);
		CircleY[i] = Math.cos(f);
	}
}
function SpinnerResize(Spinner, w, h)
{
	if(h != Spinner.CanvasH || w != Spinner.CanvasW || DPR != Spinner.CanvasDPR)
	{
		Spinner.Canvas.style.width = w + 'px';
		Spinner.Canvas.style.height = h + 'px';
		Spinner.Canvas.width = w * DPR;
		Spinner.Canvas.height = h * DPR;
		Spinner.Canvas.getContext('2d').scale(DPR, DPR);
		Spinner.CanvasBack.style.width = w + 'px';
		Spinner.CanvasBack.style.height = h + 'px';
		Spinner.CanvasBack.width = w * DPR;
		Spinner.CanvasBack.height = h * DPR;

		Spinner.CanvasBack.getContext('2d').scale(DPR, DPR);
		Spinner.CanvasH = h;
		Spinner.CanvasW = w;
		Spinner.CanvasDPR = DPR;
	}
}
function SpinnerCircle(Phase0, Shift0, Phase1, Shift1, Phase2, Shift2)
{
	return {"Phase0" : Phase0, "Shift0": Shift0, "Phase1":Phase1, "Shift1":Shift1, "Phase2": Phase2, "Shift2": Shift2, "Q": 0, "QQ": 0, "A0":0, "A1":0, "A2":0};
}

function SpinnerInit(Blur, Color)
{
	var Spinner = {};
	Spinner.x = 0;
	Spinner.y = 0;
	Spinner.Canvas = document.createElement('canvas');
	Spinner.CanvasBack = document.createElement('canvas');
	Spinner.CanvasOffscreenData;
	Spinner.CanvasW = -1;
	Spinner.CanvasH = -1;
	Spinner.CanvasDPR = -1;
	Spinner.Circles = [];
	var x = 1 / 4;
	var y = 0.07;
	Spinner.Circles.push(SpinnerCircle(x, 0, y, 0, 0.0, 0.25));
	Spinner.Circles.push(SpinnerCircle(2/4, 0, 0, 0, 0.14, 0.25));
	Spinner.Fade = 0;
	Spinner.Blur = Blur;
	Spinner.Color = Color;
	return Spinner;
}

var SpinnerCorner = SpinnerInit(0.8, 'white');
var SpinnerLoadButton = SpinnerInit(0, 'white');
var SpinnerText1 = SpinnerInit(0.0, 'red');
var SpinnerText0 = SpinnerInit(0.0, 'red');


function SpinnerShow()
{
	return SymbolState != null && (SymbolState.q == 1 || SymbolState.r != SymbolState.f || FunctionQueryLastRequest != FunctionQueryReceived);
}

function SpinnerDraw(Enable, context, Spinner, X, Y, w, h)
{
	SpinnerResize(Spinner, w, h);
	var ContextSpinner = Spinner.Canvas.getContext('2d');


	var now = Date.now();
	if(!ContextSpinner.last)
	{
		ContextSpinner.last = now;
	}
	var DT = now - ContextSpinner.last;
	ContextSpinner.last = now;
	if(Enable)
	{
		Spinner.Fade += DT / 200;
	}
	else
	{
		Spinner.Fade -= DT / 1000;
	}
	Spinner.Fade = Math.max(0, Spinner.Fade);
	Spinner.Fade = Math.min(1, Spinner.Fade);
	var F = Spinner.Fade * Spinner.Fade;

	if(Spinner.Fade <= 0)
		return;
	ContextSpinner.clearRect(0, 0, w, h);
	if(Spinner.Blur > 0)
	{
		ContextSpinner.globalAlpha = Spinner.Blur;
		ContextSpinner.drawImage(Spinner.CanvasBack, 0, 0, w, h);
		ContextSpinner.globalAlpha = 1.0;
	}
	var twopi = 2 * Math.PI;
	for(var jjj = 0; jjj < Spinner.Circles.length; ++jjj)
	{
		var C = Spinner.Circles[jjj];
		InitCircle();
		var xs = CircleX;
		var ys = CircleY;
		var scale = (w-2) / 2;
		var offset = (w) / 2;
		var SPEED = 100.0;

		var angle = (C.Q / SPEED) * twopi;

		var QQSPEED = 1000;

		C.A0 += (twopi * DT / 1000.0) * C.Phase0;
		if(C.A0 > twopi)
			C.A0 -= twopi;
		C.A1 += (twopi * DT / 1000.0) * C.Phase1;
		if(C.A1 > twopi)
			C.A1 -= twopi;
		C.A2 += (twopi * DT / 1000.0) * C.Phase2;
		if(C.A2 > twopi)
			C.A2 -= twopi;

		var A0 = C.A0 + twopi * C.Shift0;
		var A1 = C.A1 + twopi * C.Shift1;
		var A2 = C.A2 + twopi * C.Shift2;
		var m = Math.cos(A0);
		var m0 = Math.sin(A2);
		var cs = Math.cos(A1);
		var ss = Math.sin(A1);
		for(var i = 0; i < xs.length; ++i)
		{
			var x = xs[i] * m;
			var y = ys[i] * m0;
			xs[i] = cs * scale * x - ss * scale * y;
			ys[i] = ss * scale * x + cs * scale * y;
			xs[i] += offset;
			ys[i] += offset;
		}
		ContextSpinner.strokeStyle = Spinner.Color;
		ContextSpinner.beginPath();		
		ContextSpinner.moveTo(xs[0], ys[0]);
		for(var i = 1; i < CircleSteps; ++i)
		{
			ContextSpinner.lineTo(xs[i], ys[i]);
		}
		ContextSpinner.lineTo(xs[0], ys[0]);
		ContextSpinner.stroke();


	}
	context.globalAlpha = F;
	context.drawImage(Spinner.Canvas, X, Y, w, h);
	context.globalAlpha = 1;
	var tmp = Spinner.CanvasBack;
	Spinner.CanvasBack = Spinner.Canvas;
	Spinner.Canvas = tmp;
}


function DrawMenuPatched()
{
	if(FilterInputValueLast != FilterInput.value)
	{
		nOffsetMenuPatched = 0;
	}

	var FilterArray = CreateFilter(FilterInput.value);
	var context = CanvasDetailedView.getContext('2d');
	var nColorIndex = 0;
	var SizeInfo = TimerMenuSize();
	SizeInfo.x = MenuItems[SubMenuPatched].x;
	SizeInfo.y = MenuItems[SubMenuPatched].y;
	var Y = SizeInfo.y;
	var Width = SizeInfo.w;
	Width = Math.max(300, Width);
	Width = 10+ MeasureArray(Width, FunctionsInstrumented, function(s, i){return s + " " + FunctionsInstrumentedModule[i];} );

	if(Width + SizeInfo.x + 50 > nWidth)
	{
		Width = nWidth - (SizeInfo.x+50);
	}
	SizeInfo.w = Width;
	var MaxStringLength = Math.floor(Width/(FontWidth));

	var Selection = null;
	var X = SizeInfo.x;
	context.fillStyle = nBackColors[0];
	TextY = Y+BoxHeight-FontAscent;
	context.fillRect(X, Y, Width, BoxHeight);

	var bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight && MouseX-X < 100;
	context.fillRect(X, Y, 99, BoxHeight);
	context.textAlign = 'center';
	context.fillStyle = 'white';
	context.fillText("Patched functions", X + Width/2, TextY);

	Y += BoxHeight;
	MoveFilterInputDiv(SizeInfo.x, Y, SizeInfo.w-6);
	Y += 45;

	bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight;
	var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
	var TextY = Y+BoxHeight-FontAscent;
	var YClear = Y;
	var TextYClear = TextY;
	var bgcolorClear = bgcolor;
	var YStart = Y-BoxHeight*2;

	nColorIndex = 1-nColorIndex;

	Y -= nOffsetMenuFunctions + 10;
	var Selection = -1;

	var StringColor = function(Name)
	{
		var h = StringHash(Name);
		var cidx = h % 360;
		return cidx;
	};

	var GetColor = function(Str)
	{
		var cidx = StringColor(Str); 
		return "hsl(" + cidx + ",50%, 70%)";
	};
	for(var i = 0; i < FunctionsInstrumented.length; ++i)
	{
		var Name = FunctionsInstrumented[i];
		var ModuleName = FunctionsInstrumentedModule[i];
		if(FilterMatch(FilterArray, ModuleName + " " + Name))
		{

			var Color = GetColor(Name);
			var ColorModule = GetColor(Name);
			if(Y >= YStart)
			{
				bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight;
				bgcolor = bMouseIn && MouseX > X ? nBackColorOffset : nBackColors[nColorIndex];
				TextY = Y+BoxHeight-FontAscent;

				context.fillStyle = bgcolor;
				context.fillRect(X, Y, Width, BoxHeight);
				context.fillStyle = Color;
				context.textAlign = 'right';
				context.fillText(Name, X + Width - 2, TextY);
				context.textAlign = 'left';
				context.fillStyle = ColorModule;
				context.fillText(ModuleName, X, TextY);
			}
			Y += BoxHeight;
			nColorIndex = 1-nColorIndex;
		}
	}
	context.textAlign = 'left';
	SizeInfo.h = Y-SizeInfo.y;
	return SizeInfo;
}


function DrawMenuModules()
{
	if(FilterInputValueLast != FilterInput.value)
	{
		nOffsetMenuModules = 0;
	}
	FilterInputValueLast = FilterInput.value;
	var M = ModuleState;
	var FilterArray = CreateFilter(FilterInput.value);
	var context = CanvasDetailedView.getContext('2d');
	var nColorIndex = 0;
	var SizeInfo = TimerMenuSize(); //broken, fix!

	SizeInfo.x = MenuItems[SubMenuModules].x;
	SizeInfo.y = MenuItems[SubMenuModules].y;
	var Y = SizeInfo.y;
	var Width = SizeInfo.w;
	Width = Math.max(300, Width);
	Width = 10+ MeasureArray(Width, M, function(s){return s.n + " " + s.s;} );

	if(Width + SizeInfo.x + 50 > nWidth)
	{
		Width = nWidth - (SizeInfo.x+50);
	}
	SizeInfo.w = Width;


	var MaxStringLength = Math.floor(Width/(FontWidth));


	var X = SizeInfo.x;
	DrawSymbolHeaderMenu(context, X, Y, Width);

	Y += BoxHeight;
	MoveFilterInputDiv(SizeInfo.x, Y, SizeInfo.w-6);
	Y += 45;

	let bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight;
	var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
	var TextY = Y+BoxHeight-FontAscent;
	var YClear = Y;
	var TextYClear = TextY;
	var bgcolorClear = bgcolor;
	var YStart = Y-BoxHeight*2;

	nColorIndex = 1-nColorIndex;

	Y -= nOffsetMenuModules + 10;
	var Selection = -1;

	var StringColor = function(Name)
	{
		var h = StringHash(Name);
		var cidx = h % 360;
		return cidx;
	};

	for(var i = 0; i < M.length; ++i)
	{
		var Name = M[i].n;
		var Count = M[i].s;
		var Prc = M[i].p;
		if(Prc<0||Prc>1)
			Prc = 0;
		let cidx = StringColor(Name);
		let Color = "hsl(" + cidx + ",50%, 70%)";
		let C1 = "hsl(" + cidx + ",30%, 40%)";
		var N = Name;
		if(N.length > MaxStringLength)
		{
			N = N.substring(N.length - MaxStringLength);
		}
		if(FilterMatch(FilterArray, Name))
		{

			if(Y >= YStart)
			{
				bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight;
				bgcolor = bMouseIn && MouseX > X ? nBackColorOffset : nBackColors[nColorIndex];
				TextY = Y+BoxHeight-FontAscent;

				context.fillStyle = bgcolor;
				context.fillRect(X, Y, Width, BoxHeight);
				if(Prc>0 && Prc <= 1)
				{
					context.fillStyle = C1;
					context.fillRect(X, Y, Prc*Width, BoxHeight);
				}
				context.fillStyle = 'white';
				context.textAlign = 'right';
				context.fillText(Count + '', X + Width - 2, TextY);
				context.textAlign = 'left';
				context.fillStyle = Color;
				context.fillText(N, X, TextY);
				if(bMouseIn)
				{
					Selection = i;
				}
			}
			Y += BoxHeight;
			nColorIndex = 1-nColorIndex;
		}
	}
	context.textAlign = 'left';
	if(Selection != -1 && MouseReleased)
	{
		WSSendMessage("L" + M[Selection].n);
	}
	SizeInfo.h = Y-SizeInfo.y;
	return SizeInfo;
}
function DrawSymbolHeaderMenu(context, X, Y, Width)
{
	context.fillStyle = nBackColors[0];
	let TextY = Y+BoxHeight-FontAscent;
	context.fillRect(X+100, Y, Width-100, BoxHeight);
	let bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight && MouseX-X < 100;
	if(bMouseIn)
	{
		context.fillStyle = nBackColorOffset;
	}

	context.fillRect(X, Y, 99, BoxHeight);


	let NumSymbols = 0;
	let SymbolStateString = "Load all";
	let SymbolButtonActive = 1;
	let SymbolSpinnerActive = 0;
	let FinishedSymbols = 0;
	if(SymbolState)
	{
		FinishedSymbols = SymbolState.f;
		var s = SymbolState.s;
		NumSymbols = SymbolState.l;
		SymbolButtonActive = 1;
		if(SymbolState.q == 1 || FunctionQueryLastRequest != FunctionQueryReceived)
		{
			SymbolStateString = "Querying";
			SymbolButtonActive = 0;
			SymbolSpinnerActive = 1;
		}
		else if(SymbolState.f != SymbolState.r)
		{
			SymbolButtonActive = 0;
			SymbolSpinnerActive = 1;
			SymbolStateString = "Loading";
		}
		else if(SymbolState.f == ModuleState.length)
		{
			SymbolButtonActive = 0;
			SymbolStateString = "Loaded";
		}
	}
	let LoadButtonAnimation  = LoadSymbolAnimUpdateAndGet();

	context.textAlign = 'right';
	context.fillStyle = 'white';
	context.fillText(NumSymbols + " Symbols " + FinishedSymbols + "/" + ModuleState.length + " Modules", X+Width-2, TextY);
	context.textAlign = 'center';

	if(LoadButtonAnimation>0)
	{ 
		var c = "hsl(0,0%," + (LoadButtonAnimation * 100)+"%)";
		context.fillStyle = c;
	}
	if(SymbolButtonActive)
	{
		if(bMouseIn && MouseReleased)
		{
			WSSendMessage("S"); //load symbols
		}
		context.fillText(SymbolStateString, X + 50, TextY);
	}
	else
	{
		context.fillStyle = 'grey';
		context.fillText(SymbolStateString, X + 50, TextY);
	}
	SpinnerDraw(SymbolSpinnerActive, context, SpinnerLoadButton, X+2, Y,  FontHeight, FontHeight );


}

function DrawMenuFunctions()
{
	if(FilterInputValueLast != FilterInput.value)
	{
		nOffsetMenuFunctions = 0;
		var m = FilterInput.value.trim();
		if(m != "")
		{
			if(SymbolState && SymbolState.r == SymbolState.f)
			{
				var Req = ++FunctionQueryLastRequest;
				var Q = "q" + Req + "x" + m;
				WSSendMessage(Q);
				FunctionQueryPending = null;
			}
			else
			{
				FunctionQueryPending = m; 
				if(SymbolState) //wtf?
				{
					LoadSymbolAnimRestart();
				}
			}
		}
	}

	FilterInputValueLast = FilterInput.value;
	var FF = FunctionQueryArray;
	var FilterArray = CreateFilter(FilterInput.value);
	var context = CanvasDetailedView.getContext('2d');
	var nColorIndex = 0;
	var SizeInfo = TimerMenuSize(); //broken, fix
	SizeInfo.x = MenuItems[SubMenuFunctions].x;
	SizeInfo.y = MenuItems[SubMenuFunctions].y;
	var Y = SizeInfo.y;
	var Width = SizeInfo.w;
	Width = Math.max(300, Width);
	Width = 10+ MeasureArray(Width, FF, function(s){return s.n + " " + s.m;} );

	if(Width + SizeInfo.x + 50 > nWidth)
	{
		Width = nWidth - (SizeInfo.x+50);
	}
	SizeInfo.w = Width;
	var MaxStringLength = Math.floor(Width/(FontWidth));

	var Selection = null;
	var X = SizeInfo.x;
	
	{
		DrawSymbolHeaderMenu(context, X, Y, Width);
	}


	Y += BoxHeight;
	MoveFilterInputDiv(SizeInfo.x, Y, SizeInfo.w-6);
	Y += 45;

	let bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight;
	var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
	var TextY = Y+BoxHeight-FontAscent;
	var YClear = Y;
	var TextYClear = TextY;
	var bgcolorClear = bgcolor;
	var YStart = Y-BoxHeight*2;

	nColorIndex = 1-nColorIndex;

	Y -= nOffsetMenuFunctions + 10;
	var Selection = -1;

	var StringColor = function(Name)
	{
		var h = StringHash(Name);
		var cidx = h % 360;
		return cidx;
	};

	for(var i = 0; i < FF.length; ++i)
	{
		if(true)
		{
			var Name = FF[i].n;
			var ShortName = FF[i].sn;
			var Color = FF[i].c;
			var ColorModule = FF[i].cm;
			var ModuleName = FF[i].m;
			if(!Color || !ColorModule)
			{
				var cidx = StringColor(ShortName); 
				FF[i].c = "hsl(" + cidx + ",50%, 70%)";
				FF[i].rgb = ConvertHslToRGB(cidx/ 360, 0.5, 0.7);
				var cidxModule = StringColor(ModuleName); 
				FF[i].cm = "hsl(" + cidxModule + ",50%, 70%)";
				FF[i].rgbm = ConvertHslToRGB(cidxModule/ 360, 0.5, 0.7);
				Color = FF[i].c;
				ColorModule = FF[i].cm;
			}
			var UseAlt = 0;
			var N = UseAlt ? Name : ShortName;
			if(N.length > MaxStringLength)
			{
				N = N.substring(N.length - MaxStringLength);
			}
			var E = false;
			if(true)
			{

				if(Y >= YStart)
				{
					bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight;
					bgcolor = bMouseIn && MouseX > X ? nBackColorOffset : nBackColors[nColorIndex];
					TextY = Y+BoxHeight-FontAscent;

					context.fillStyle = bgcolor;
					context.fillRect(X, Y, Width, BoxHeight);
					context.fillStyle = Color;
					context.textAlign = 'right';
					context.fillText(N, X + Width - 2, TextY);
					context.textAlign = 'left';
					context.fillStyle = ColorModule;
					context.fillText(ModuleName, X, TextY);
					if(bMouseIn)
					{
						context.fillStyle = Color;
						var bgcolorin = bMouseIn && MouseX < X ? nBackColorOffset : nBackColors[nColorIndex];

						Selection = i;
						var W = context.measureText('>>').width;
						W = Math.max(W, BoxHeight) + 2;
						var Corner = X - W - 1;
						if(Corner < SizeInfo.x)
						{
							SizeInfo.w += SizeInfo.x - Corner;
							SizeInfo.x = Corner;
						}
						context.fillStyle = bgcolorin;
						context.fillRect(Corner, Y, W, BoxHeight);
						context.fillStyle = Color;
						context.textAlign = 'center';
						context.fillText(">>", Corner + Math.floor(W / 2), TextY);

						context.textAlign = 'left';
					}
				}
				Y += BoxHeight;
				nColorIndex = 1-nColorIndex;
			}
		}
	}

	context.textAlign = 'left';
	if(Selection != -1 && MouseReleased)
	{
		if(KeyShiftDown || MouseX < X)
		{
			WSSendMessage("I" + FF[Selection].a + ' ' + FF[Selection].rgb + ' ' + FF[Selection].m + '!' + FF[Selection].n);		
		}
		else
		{
			WSSendMessage("i" + FF[Selection].a + ' ' + FF[Selection].rgb + ' ' + FF[Selection].m + '!' + FF[Selection].n);		
		}
	}
	SizeInfo.h = Y-SizeInfo.y;
	return SizeInfo;
}

function DrawMenuGeneric(Elements, Active, OnClick, x, y, Elements2)
{
	var context = CanvasDetailedView.getContext('2d');
	var nColorIndex = 0;
	if(Elements2.length != Elements.length)
	{
		Elements2 = null;
	}

	var h = FontHeight * Elements.length;
	var w = 20;
	var w2 = 0;
	for(var i = 0; i < Elements.length; ++i)
	{
		var m = context.measureText(Elements[i]).width;
		w = w > m ? w : m;
		if(Elements2)
		{
			m = context.measureText(Elements2[i]).width;
			w2 = w2 > m ? w2 : m;
		}
	}
	w += 10 + w2;
	var SizeInfo = MenuSize(w);
	SizeInfo.x = x;
	SizeInfo.y = y;
	var X = x;
	var Y = y;


	for(var i = 0; i < Elements.length; ++i)
	{
		var Selected = Active == i;
		var Name = Elements[i];
		var bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight;
		var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
		var TextY = Y+BoxHeight-FontAscent;
		context.fillStyle = Selected?'white':bgcolor;
		context.fillRect(X-2, Y, w+4, BoxHeight);
		context.fillStyle = bgcolor;
		context.fillRect(X, Y, w, BoxHeight);
		context.fillStyle = 'white';
		context.fillText(Name, X + 2, TextY);
		if(Elements2)
		{
			context.textAlign = "right";
			context.fillText(Elements2[i], X + w , TextY);
			context.textAlign = "left";
		}
		context.fillText(Name, X + 2, TextY);
		if(bMouseIn && MouseReleased)
		{
			OnClick(i, Name);
		}
		Y += BoxHeight;
		nColorIndex = 1-nColorIndex;
	}
	SizeInfo.h = Y - SizeInfo.y;
	return SizeInfo;
}

function DrawMenuGroup()
{
	if(FilterInputValueLast != FilterInput.value)
	{
		nOffsetMenuGroup = 0;
	}
	FilterInputValueLast = FilterInput.value;

	var context = CanvasDetailedView.getContext('2d');
	var nColorIndex = 0;
	var Selection = null;
	var SizeInfo = GroupMenuSize();
	SizeInfo.x = MenuItems[SubMenuGroup].x;
	SizeInfo.y = MenuItems[SubMenuGroup].y;
	var Y = SizeInfo.y;
	var X = SizeInfo.x;
	var Width = SizeInfo.w;
	var FilterArray = CreateFilter(FilterInput.value);
	MoveFilterInputDiv(SizeInfo.x-2, SizeInfo.y, SizeInfo.w-1);
	var YStart = Y+20;
	Y += 35;
	Y -= nOffsetMenuGroup;


	function DrawMenuElement(Selected, Name, color, Indent)
	{
		var bMouseIn = MouseY >= Y && MouseY < Y + BoxHeight;
		var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
		context.fillStyle = Selected?'white':bgcolor;
		context.fillRect(X-2, Y, Width+4, BoxHeight);
		context.fillStyle = bgcolor;
		context.fillRect(X, Y, Width, BoxHeight);
		context.fillStyle = color;
		if(!Indent) Indent = 0;
		context.fillText(Name, X + Indent*FontWidth, Y+BoxHeight-FontAscent);
		nColorIndex = 1-nColorIndex;
		Y += BoxHeight;
		return bMouseIn;
	}
	function DrawMenuRecursive(Index, Indent, categorymatch)
	{
		ProfileEnter("DrawMenuRecursive");		
		var v = TimerArray[Index];
		if(v.idtype == TYPE_TIMER || v.idtype == TYPE_COUNTER)
		{
			return;
		}
		if(v.idtype == TYPE_GROUP && !categorymatch)
		{
			if(!FilterMatch(FilterArray, v.name))
			{
				return;
			}
		}
		var catmatch = 0;
		if(v.idtype == TYPE_CATEGORY)
		{
			if(FilterMatch(FilterArray, v.name))
			{
				catmatch = 1;
			}
		}
		var Closed = 0;
		if(Index > 0)
		{
			if(Y > YStart)
			{
				if(DrawMenuElement(v.e, v.name, v.color, Indent))
				{
					Selection = v.id;
				}
			}
			else
			{
				Y += BoxHeight;
			}
		}

		if(!Closed)
		{
			var ChildIndex = v.firstchild;
			while(ChildIndex != -1)
			{
				DrawMenuRecursive(ChildIndex, Indent + 1, catmatch);
				ChildIndex = TimerArray[ChildIndex].sibling;
			}
		}
		ProfileLeave();
	}
	DrawMenuRecursive(0, -1);
	if(Selection && MouseReleased)
	{
		WSSendMessage("c" + Selection);
	}
	SizeInfo.h = Y - SizeInfo.y;
	return SizeInfo;
}

function ShowMenu()
{
	if(!Settings.ViewCompressed)
		return true;
	else
	{
		var Time = new Date();
		var Delta = Time - MouseMoveTime;
		console.log('delta time is ' + Delta);
		return Delta < 2000;
	}
}

function DrawMenu()
{
	if(WSConnected && WS && WS.readyState == 1)
	{
		var context = CanvasDetailedView.getContext('2d');
		var nColorIndex = 0;
		var Y = 50;
		var Width = 300;
		var Selection = null;
		if(!ShowMenu())
		{
			return;
		}
		ProfileEnter("DrawMenu");
		DrawTopMenu();
		var MenuRect = WindowRect(0,0,nWidth,nHeight);
		if(SubMenuActive != -1)
		{
			MouseMoveTime = new Date();
		}
		if(SubMenuActive == SubMenuGroup)
		{
			MenuRect = DrawMenuGroup();
		}
		else if(SubMenuActive == SubMenuTimers)
		{
			MenuRect = DrawMenuTimer();
		}
		else if(SubMenuActive == SubMenuModules)
		{
			MenuRect = DrawMenuModules();
		}
		else if(SubMenuActive == SubMenuFunctions)
		{
			MenuRect = DrawMenuFunctions();
		}
		else if(SubMenuActive == SubMenuPatched)
		{
			MenuRect = DrawMenuPatched();
		}
		else if(SubMenuActive == SubMenuSettings)
		{
			MenuRect = DrawMenuSettings();
		}
		else if(SubMenuActive == SubMenuViews)
		{
			MenuRect = DrawMenuViews();
		}
		else if(SubMenuActive == SubMenuPresets)
		{
			MenuRect = DrawMenuPresets();
		}
		else if(SubMenuActive == SubMenuColumns)
		{
			MenuRect = DrawMenuColumns();
		}
		else if(SubMenuActive == SubMenuCapture)
		{
			MenuRect = DrawMenuCapture();
		}
		else if(SubMenuActive == SubMenuGraphSettings)
		{
			MenuRect = DrawMenuGraphSettings();
		}
		var Grow = 10;
		MenuRect.x -= Grow;
		MenuRect.y -= Grow;
		MenuRect.h += 2*Grow;
		MenuRect.w += 2*Grow;
		var MouseMoved = MouseX != SubMenuMouseX || MouseY != SubMenuMouseY;
	
		if(MouseInRect(MenuRect) || !MouseMoved)
		{
			SubMenuTimeout = new Date();
			SubMenuMouseX = MouseX;
			SubMenuMouseY = MouseY;
		}
		else
		{
			var Time = new Date() - SubMenuTimeout;
			var Dest = SubMenuTimeoutBase * 1000;
			if(Time > Dest)
			{
				EnableMenu(-1);
			}
		}
		if(0)//debugging of menu extents. dont delete
		{
			context.strokeStyle = 'red';
			context.beginPath();
			context.moveTo(MenuRect.x,MenuRect.y);
			context.lineTo(MenuRect.x + MenuRect.w,MenuRect.y);
			context.lineTo(MenuRect.x + MenuRect.w,MenuRect.y+MenuRect.h);
			context.lineTo(MenuRect.x,MenuRect.y+MenuRect.h);
			context.lineTo(MenuRect.x,MenuRect.y);
			context.stroke();
		}
		SpinnerDraw(SpinnerShow(), context, SpinnerCorner, 0, nHeight-20,  20, 20);
		ProfileLeave();
	}
}

function DrawConnectionStatus()
{
	if(WSConnected && WS && WS.readyState == 1)
	{
		if(!ProfileMode)
			return;
	}
	var Strings = new Array();
	Strings.push("Status");
	if(WSConnected && WS && WS.readyState == 1)
	{
		Strings.push("[X]");
	}
	else
	{
		ConnectionIdx = (ConnectionIdx + 1 ) % ConnectionStr.length;
		Strings.push("[" + ConnectionStr[ConnectionIdx]+"]");
	}
	Strings.push("Port");
	Strings.push("" + WSPort);
	Strings.push("Path");
	Strings.push("" + WSPath);
	Strings.push("Sends");
	Strings.push("" + WSSend);
	Strings.push("Receives");
	Strings.push("" + WSReceive);
	Strings.push("SendBytes");
	Strings.push("" + WSSendBytes);
	Strings.push("ReceiveBytes");
	Strings.push("" + WSReceiveBytes);
	Strings.push("Seconds");
	Strings.push("" + WSSeconds);
	DrawToolTip(Strings, CanvasDetailedView, 50000, 0);
}
function DrawActiveToolTip()
{
	if(SubMenuActive == -1)
	{
		if(ToolTipCallback && SubMenuActive == -1)
		{
			var Strings = ToolTipCallback(CanvasDetailedView, MouseX, MouseY);
			if(Strings)
			{
				DrawToolTip(Strings, CanvasDetailedView, MouseX, MouseY);
			}
		}
	}
	ToolTipCallback = null;
}
function UpdateSettings()
{
	if(Settings.AutomaticReference)
	{
		if( Math.abs(ReferenceGraph - ReferenceGraphAutomatic) > 0.02 ||
			Math.abs(ReferenceHistory - ReferenceHistoryAutomatic) > 0.02)
		{
			RequestDraw();
		}
		ReferenceGraph = 0.9 * ReferenceGraph + 0.1 * ReferenceGraphAutomatic;
		ReferenceHistory = 0.9 * ReferenceHistory + 0.1 * ReferenceHistoryAutomatic;
		ReferenceBar = 0.9 * ReferenceBar + 0.1 * ReferenceBarAutomatic;
		var TimerMap = FrameData.TimerMap;
		if(!TimerMap)
			return;


		for(var key in TimerMap)
		{
			var TimerState = TimerMap[key];
			if(!TimerState.historymaxsoft)
			{
				TimerState.historymaxsoft = TimerState.historymax;
			}
			else
			{
				TimerState.historymaxsoft = 0.9 * TimerState.historymaxsoft + 0.1 * TimerState.historymax;
			}
		}

	}
	else
	{
		ReferenceGraph = Settings.ReferenceTime;
		ReferenceHistory = Settings.ReferenceTime;
		ReferenceBar = Settings.ReferenceTime;
	}
	if(Settings.AggregateFrames != AggregateFrames)
	{
		WSSendMessage("a" + Settings.AggregateFrames);
		AggregateFrames = Settings.AggregateFrames;
	}
}

var PendingDraw = 0;

function Draw()
{
	CaptureUpdate();
	PendingDraw = 0;
	ProfileEnter("Total");

	UpdateSettings();

	if(WSConnected && WS && WS.readyState == 1)
	{
		DrawViews();
		DrawMessage();	
		DrawMenu();
	}
	else
	{
		var context = CanvasDetailedView.getContext('2d');
		context.clearRect(0, 0, nWidth, nHeight);
		DrawMessage();
	}

	DrawConnectionStatus();
	DrawPlotf(CanvasDetailedView);
	DrawActiveToolTip();


	ProfileLeave();
	ProfileModeDraw(CanvasDetailedView);
	ProfileModeClear();


	PlotfClear();

	MouseReleased = false;
}
function RequestDraw()
{
	if(!PendingDraw)
	{
		PendingDraw = 1;
		requestAnimationFrame(Draw);
	}
}




function WSOpen(event)
{
	SetMessage("Connected!", 1000);
	WSSend = 0;
	WSReceive = 0;
	WSSendBytes = 0;
	WSReceiveBytes = 0;
	WSIsOpen = 1;

	Empty = {"id":0, "w":0, "depth":0, "sibling":-1,"parent":-1,"firstchild":-1};
	TimerArray = [];
	TimerArray.push(Empty);
	WidthArray = [];
	CounterArray = [];
	WidthArray[TYPE_NONE] = 0;
	WidthArray[TYPE_TIMER] = 0;
	WidthArray[TYPE_GROUP] = 0;
	WidthArray[TYPE_CATEGORY] = 0;
	WidthTree = FontWidth;
	SymbolState = null;
	FunctionQueryPending = 0;
	FunctionQueryReceived = 0;
	FunctionQueryLastRequest = 0;
	FunctionsInstrumented = [];
	FunctionsInstrumentedModule = [];
	FunctionsInstrumentedUnmangled = [];

	FilterInputTimersValue = "";
	FilterInputGroupsValue = "";
	FilterInputFunctionsValue = "";
	FilterInput.value = "";
	FunctionQueryArray = [];
	ModuleState = [];

	ResetFrameData();
	window.document.title = "MP:" + WSHost;

	if(PresetToLoad && PresetToLoad != "")
	{
		LoadPreset(PresetToLoad, PresetToLoadRO);
	}
}

function SplitIdTop(v)
{
	return v >> 24; // todo: verify
}

function SplitIdBottom(v)
{
	return v & 0xffffff; // todo: verify
}

function GetTimer(id)
{
	for(var i = 0; i < TimerArray.length; ++i)
	{
		if(TimerArray[i].id == id)
		{
			return i;
		}
	}
	return null;
}
function IsGroup(id)
{
	var idx = GetTimer(id);
	return TimerArray[idx].idtype == TYPE_GROUP;
}

function UpdateActive()
{
	GroupsEnabled = 0;
	TimersEnabled = 0;
	for(var i = 0; i < TimerArray.length; ++i)
	{
		var T = TimerArray[i];
		if(T.e)
		{
			switch(T.idtype)
			{
				case TYPE_GROUP:
					GroupsEnabled++;
				break;
				case TYPE_TIMER:
					TimersEnabled++;
				break;
			}
		}
	}
}
function UpdateEnabledTimer(idx)
{
	UpdateActive();
	var type = TimerArray[idx].idtype;
	var enabled = TimerArray[idx].e;
	if(TimerArray[idx].idtype != TYPE_TIMER)
	{
		return;
	}
	var AutoCaptureSourceValue = null;
	if(AutoCaptureSourceIndex >= 0 && AutoCaptureSourceIndex < EnabledArray.length)
	{
		AutoCaptureSourceValue = EnabledArray[AutoCaptureSourceIndex];
	}

	var i = EnabledArray.indexOf(idx);
	if(enabled)
	{
		if(i == -1)
		{
			EnabledArray.unshift(idx);
		}
	}
	else
	{
		if(i != -1)
		{
			EnabledArray.splice(i, 1);
		}
	}
	var NewIndex = AutoCaptureSourceValue != null ? EnabledArray.indexOf(AutoCaptureSourceValue) : -1;
	AutoCaptureSourceIndex = NewIndex;
}


function EnableTimer(T)
{
	var idx = GetTimer(T.id);
	if(idx != null && idx>=0)
	{
		TimerArray[idx].e = T.e;
		UpdateEnabledTimer(idx);
	}
	else
	{
		console.log('unknown enable message');
	}
}
function MeasureWidth(str)
{
	var context = CanvasDetailedView.getContext('2d');
	return context.measureText(str).width;
}
function AddTimer(T)
{
	if(!T.color || T.color == "")
	{
		T.color = ColorFromString(T.name, 40, 50);
	}
	console.log("Added timer", T.name);
	var idx = TimerArray.length;
	var existing = GetTimer(T.id);
	if(existing)
	{
		idx = existing;
	}
	else
	{
		TimerArray[idx] = T;
	}
	TimerArray[idx].time = 0;
	TimerArray[idx].excl = 0;
	TimerArray[idx].average = 0;
	TimerArray[idx].max = 0;
	TimerArray[idx].min = 0;
	TimerArray[idx].total = 0;
	TimerArray[idx].exclaverage = 0;
	TimerArray[idx].exclmax = 0;
	TimerArray[idx].excltotal = 0;
	TimerArray[idx].spike = 0;
	TimerArray[idx].callaverage = 0;
	TimerArray[idx].callexclaverage = 0;
	TimerArray[idx].callcount = 0;
	var w = MeasureWidth(T.name);
	var idtype = SplitIdTop(T.id);
	var idelement = SplitIdBottom(T.id);
	TimerArray[idx].idtype = idtype;
	TimerArray[idx].idelement = idelement;
	TimerArray[idx].w = w;
	TimerArray[idx].wtree = w;
	if(!existing)
	{
		TimerArray[idx].sibling = -1;
		TimerArray[idx].parent = -1;
		TimerArray[idx].firstchild = -1;
	}
	if(w > WidthArray[idtype])
	{
		WidthArray[idtype] = w;
	}
	UpdateEnabledTimer(idx);
	var wparent = 0;
	var pidx = GetTimer(T.pid);
	if(pidx >= 0 && !existing)
	{
		TimerArray[idx].parent = pidx;
		var Parent = TimerArray[pidx];
		var Sibling = Parent.firstchild;
		wparent = MeasureWidth(Parent.name);
		Parent.firstchild = idx;
		if(Sibling != -1)
		{
			if(TimerArray[Sibling].sibling == idx)
			{
				debugger;
			}
		}
		TimerArray[idx].sibling = Sibling;
		TimerArray[idx].wtree += Parent.depth * FontWidth;
		TimerArray[idx].depth = Parent.depth + 1;
		if(TimerArray[idx].wtree > WidthTree)
		{
			WidthTree = TimerArray[idx].wtree;
		}
		TimerArray[idx].sortkey = Parent.name + " __ " + T.name;
	}
	else
	{
		TimerArray[idx].sortkey = T.name;
	}
	TimerArray[idx].wparent = wparent;
	TimerArray[idx].wtotal = wparent + w;

	if(idtype == TYPE_COUNTER)
	{
		if(idelement != CounterArray.length)
		{
			debugger;
		}
		CounterArray.push(idx);
		TimerArray[idx].formattedlimit = FormatCounter(TimerArray[idx].format, TimerArray[idx].limit);
		CounterLimitWidth = Math.max(CounterLimitWidth, TimerArray[idx].formattedlimit.length * (FontWidth+1));
		CounterNameWidth = Math.max(CounterNameWidth, (TimerArray[idx].name.length + 1 + TimerArray[idx].depth) * (FontWidth+1));
		TimerArray[idx].counterhistory = {};		
		TimerArray[idx].counterhistory.history = AllocClearedArray(120);
		TimerArray[idx].counterhistory.prc = AllocClearedArray(120);		
	}
	if(idtype == TYPE_TIMER)
	{
		if(T.color == "#000000")
		{
			T.color = ColorFromString(T.name + "x" + Parent.name, 40, 50);
		}

	}
	RequestDraw();
}

function AllocClearedArray(Size)
{
	var A = new Array(Size);
	for(var i = 0; i < Size; ++i)
	{
		A[i] = 0;
	}
	return A;
}
function PushIntoArray(A, v)
{
	A.shift();
	A.push(v);
}
function ResetFrameData()
{
	FrameData = {};
	FrameData.TimerMap = {};
	FrameData.Time = AllocClearedArray(FRAME_COUNT);
	FrameData.Ids = AllocClearedArray(FRAME_COUNT);
	FrameData.Frozen = AllocClearedArray(FRAME_COUNT);
	FrameData.ThreadTime = {};
}
function GetFrameData(id)
{
	if(FrameData.TimerMap[id])
	{
		return FrameData.TimerMap[id];
	}
	else
	{
		var FD = {};
		FD.Count = AllocClearedArray(FRAME_COUNT);
		FD.Time = AllocClearedArray(FRAME_COUNT);
		FD.TimeExcl = AllocClearedArray(FRAME_COUNT);
		FD.TimeMax = AllocClearedArray(AggregateHistorySize);
		FD.TimeMin = AllocClearedArray(AggregateHistorySize);
		FD.TimeAvg = AllocClearedArray(AggregateHistorySize);
		FD.TimeExclMax = AllocClearedArray(AggregateHistorySize);
		FD.TimeExclMin = AllocClearedArray(AggregateHistorySize);
		FD.TimeExclAvg = AllocClearedArray(AggregateHistorySize);
		FD.TimeCallAvg = AllocClearedArray(AggregateHistorySize);
		FD.TimeCallExclAvg = AllocClearedArray(AggregateHistorySize);
		FD.TimeTotal = AllocClearedArray(AggregateHistorySize);
		FD.TimeExclTotal = AllocClearedArray(AggregateHistorySize);
		FD.CallCount = AllocClearedArray(AggregateHistorySize);
		FD.Percentile = new Float32Array(PERCENTILE_SAMPLES);
		FD.Percentile.fill(0.0);
		FD.PercentileMax = -1e38;
		FD.PercentileMin = 1e38;
		FD.PercentileCount = 0;
		FD.EmptyFrames = 0;
		

		FD.AggregateFrames = 0;
		FD.FrameTime = 0.0;
		FD.Aggregate = 0;
		FD.AggregateTime = 0.0;
		FD.AggregateSum = 0;
		FD.AggregateMax = 0;
		FD.AggregateMin = C_HUGE;
		FD.AggregateExclSum = 0;
		FD.AggregateExclMax = 0;
		FD.AggregateCount = 0;
		FD.AggregateExclMin = C_HUGE;



		FrameData.TimerMap[id] = FD;
		return FD;
	}
}

function ProcessCounters(C)
{
	for(var i = 0; i < CounterArray.length; ++i)
	{
		if(i > C.length)
		{
			debugger;
		}
		var idx = CounterArray[i];
		var value = C[i];
		var T = TimerArray[idx];
		T.value = value;
		if(T.minvalue == undefined)
			T.minvalue = value;
		else
			T.minvalue = Math.min(T.minvalue, value);
		if(T.maxvalue == undefined)
			T.maxvalue = value;
		else
			T.maxvalue = Math.max(T.maxvalue, value);
		T.formatted = FormatCounter(T.format, value);
		var boxprc = 1.0;
		var counterprc = 0;
		if(T.limit)
		{
			counterprc = value / T.limit;
			if(counterprc > 1.0)
			{
				boxprc = 1.0 / counterprc;
				counterprc = 1.0;
			}
			counterprc = Math.max(counterprc, 0.0);

		}
		T.boxprc = boxprc;
		T.counterprc = counterprc;
		PushIntoArray(T.counterhistory.history, value);
		var prc = T.maxvalue > T.minvalue ? (value - T.minvalue) / (T.maxvalue - T.minvalue) : 0.0;
		PushIntoArray(T.counterhistory.prc, prc);
		CounterValueWidth = Math.max(CounterValueWidth, T.formatted.length * (FontWidth+1));
	}

}


function ProcessFrame(F, Inactive)
{
	ProfileEnter("ProcessFrame");
	if(F.s)
	{
		if(F.s.l == F.s.r)
		{
			if(FunctionQueryPending)
			{
				var Req = ++FunctionQueryLastRequest;
				var Q = "q" + Req + "x" + FunctionQueryPending;
				WSSendMessage(Q);
				FunctionQueryPending = null;
			}
		}
		SymbolState = F.s;
		if(F.M)
		{
			ModuleState = F.M;
		}
		MenuItems[SubMenuFunctions].visible = function(){return true;};
		MenuItems[SubMenuModules].visible = function(){return true;};
	}
	else
	{
		MenuItems[SubMenuFunctions].visible = function(){return false;};
		MenuItems[SubMenuModules].visible = function(){return false;};
	}
	if(F.fr)
	{
		IsFrozen = 10;//allow it to stabilize after freezing
		ProfileLeave();
		return;
	}
	if(IsFrozen)
	{
		IsFrozen--;
	}
	if(!Inactive)
	{
		var TriggerAutoCapture = 0;
		PushIntoArray(FrameData.Time, F.t);
		PushIntoArray(FrameData.Ids, F.f);
		PushIntoArray(FrameData.Frozen, IsFrozen ? 1 : 0);
		var CaptureId = null;
		var AutoCapture = AutoCaptureEnabled && !IsFrozen;
		AggregateCurrent = F.a;
		if(F.m != Settings.ViewActive)
		{
			WSSendMessage("v" + Settings.ViewActive);
		}
		if(AutoCapture > 0 && null == CaptureTriggerTime && AutoCaptureCooldown < 0)
		{
			if(AutoCaptureSourceIndex == -1 && F.t > Settings.AutoCaptureTheshold)
			{
				AutoCaptureEnabled -= 1;
				TriggerAutoCapture = 1;
			}
			else if(AutoCaptureSourceIndex >= 0 && AutoCaptureSourceIndex < EnabledArray.length)
			{
				var id = TimerArray[EnabledArray[AutoCaptureSourceIndex]].id;
				var Data = F.x[id];
				if(Data && Data[0])
				{
				 	if(Data[0] > Settings.AutoCaptureTheshold)
				 	{
				 		TriggerAutoCapture = 1;
				 		AutoCaptureEnabled -= 1;
						console.log('trigger capture! ', Data[0], ' ', Settings.AutoCaptureTheshold);
				 	}
				}
			}
		}
		AutoCaptureCooldown = AutoCaptureCooldown - 1;

		var GraphTimeMax = 0;
		var GraphTimeGroupMax = 0;
		var HistoryTimeMax = 0;

		for(var i = 0; i < FrameData.Time.length; ++i)
		{
			if(0 == FrameData.Frozen[i])
			{
				HistoryTimeMax = HistoryTimeMax > FrameData.Time[i] ? HistoryTimeMax : FrameData.Time[i];
			}
		}
		var FindMaxTime = function(A, bIsGroup)
		{
			var MaxTime = 0;
			for(var i = 0; i < A.length; ++i)
			{
				if(0 == FrameData.Frozen[i])
				{
					MaxTime = MaxTime > A[i] ? MaxTime : A[i];
				}
			}
			if(bIsGroup)	
				GraphTimeGroupMax = MaxTime > GraphTimeGroupMax ? MaxTime : GraphTimeGroupMax;
			else
				GraphTimeMax = MaxTime > GraphTimeMax ? MaxTime : GraphTimeMax;
			return MaxTime;
		};
		function SetAggregateTimersInArray(FD, id)
		{
			var idx = GetTimer(id);
			var Pos = AggregateHistorySize-1;
			var T = TimerArray[idx];

			T.max = FD.TimeMax[Pos];
			T.total = FD.TimeTotal[Pos];
			T.excltotal = FD.TimeExclTotal[Pos];
			T.min = FD.TimeMin[Pos];
			T.spike = (T.average == 0 || T.max == 0) ? 0 : (100*T.max/T.average);
			T.callaverage = FD.TimeCallAvg[Pos];
			T.callcount = FD.CallCount[Pos];
			T.callexclaverage = FD.TimeCallExclAvg[Pos];
			T.exclaverage = FD.TimeExclAvg[Pos];
			T.exclmax = FD.TimeExclMax[Pos];
			T.exclmin = FD.TimeExclMin[Pos];
			T.average = FD.TimeAvg[Pos];
		};
		function SetTimersInArray(FD, id)
		{
			var idx = GetTimer(id);
			var Pos = FD.Time.length-1;
			var T = TimerArray[idx];
			T.time = FD.Time[Pos];
			T.excl = FD.TimeExcl[Pos];
			T.count = FD.Count[Pos];

		};

		let UpdateFrameDataInternal = function(id, Time, TimeExcl, Count, bIsGroup)
		{

			let FD = GetFrameData(id);
			if(!IsFrozen)
			{
				ProfileEnter("PercentileAccum");
				FD.PercentileMax = Math.max(FD.PercentileMax, Time);
				FD.PercentileMin = Math.min(FD.PercentileMin, Time);
				FD.PercentileCount += 1;
				if(Time > FD.Percentile[0])
				{
					FD.Percentile[0] = Time;
					FD.Percentile.sort( function(a,b){return a - b;} );
				}
				ProfileLeave();
			}
			PushIntoArray(FD.Time, Time);
			PushIntoArray(FD.TimeExcl, TimeExcl);
			PushIntoArray(FD.Count, Count);
			FD.historymax = FindMaxTime(FD.Time, bIsGroup);

			if((FD.Aggregate > Settings.AggregateFrames && Settings.AggregateFrames > 0) || AggregateCurrent == 0)
			{
				SetAggregateTimersInArray(FD, id);
				FD.Aggregate = 0;
				FD.AggregateSum = 0;
				FD.AggregateMax = 0;
				FD.AggregateMin = C_HUGE;
				FD.AggregateExclSum = 0;
				FD.AggregateExclMax = 0;
				FD.AggregateExclMin = C_HUGE;
				FD.AggregateCount = 0;
				
				FD.TimeMax.shift();
				FD.TimeMax.push(0);
				
				FD.TimeMin.shift();
				FD.TimeMin.push(0);

				FD.TimeAvg.shift();
				FD.TimeAvg.push(0);
				
				FD.TimeCallAvg.shift();
				FD.TimeCallAvg.push(0);
				
				FD.TimeCallExclAvg.shift();
				FD.TimeCallExclAvg.push(0);

				FD.CallCount.shift();
				FD.CallCount.push(0);

				FD.TimeTotal.shift();
				FD.TimeTotal.push(0);

				FD.TimeExclTotal.shift();
				FD.TimeExclTotal.push(0);

				FD.TimeExclMax.shift();
				FD.TimeExclMax.push(0);

				FD.TimeExclMin.shift();
				FD.TimeExclMin.push(0);

				FD.TimeExclAvg.shift();
				FD.TimeExclAvg.push(0);
			}

			FD.Aggregate += 1;
			FD.AggregateSum += Time;
			FD.AggregateMax = FD.AggregateMax > Time ? FD.AggregateMax : Time;
			FD.AggregateMin = FD.AggregateMin < Time ? FD.AggregateMin : Time;
			FD.AggregateExclSum += TimeExcl;
			FD.AggregateExclMax = FD.AggregateExclMax > TimeExcl ? FD.AggregateExclMax : TimeExcl;
			FD.AggregateExclMin = FD.AggregateExclMin < TimeExcl ? FD.AggregateExclMin : TimeExcl;
			FD.AggregateCount += Count;
			var UpdatePos = AggregateHistorySize-1;
			if(UpdatePos != FD.TimeMax.length - 1)
			{
				debugger;
			}

			FD.TimeMax[UpdatePos] = FD.AggregateMax;
			FD.TimeMin[UpdatePos] = FD.AggregateMin;
			FD.TimeAvg[UpdatePos] = FD.AggregateSum / FD.Aggregate;
			FD.TimeCallAvg[UpdatePos] = FD.AggregateCount ? FD.AggregateSum / FD.AggregateCount : 0;
			FD.TimeCallExclAvg[UpdatePos] = FD.AggregateCount ? FD.AggregateExclSum / FD.AggregateCount : 0;
			FD.TimeTotal[UpdatePos] = FD.AggregateSum;
			FD.TimeExclTotal[UpdatePos] = FD.AggregateExclSum;
			FD.CallCount[UpdatePos] = FD.AggregateCount;
			FD.TimeExclAvg[UpdatePos] = FD.Aggregate ? FD.AggregateExclSum / FD.Aggregate : 0;
			FD.TimeExclMax[UpdatePos] = FD.AggregateExclMax;
			FD.TimeExclMin[UpdatePos] = FD.AggregateExclMin;

			if(Settings.AggregateFrames <= 0)
			{
				SetAggregateTimersInArray(FD, id);
			}
			SetTimersInArray(FD, id);			
		};

		for(var id in F.x)
		{
			var Data = F.x[id];
			var Time = Data[0];
			var TimeExcl = Data[1];
			var Count = Data[2];
			UpdateFrameDataInternal(id, Data[0], Data[1], Data[2]);
		}

		var groups = {};
		for(var i = 0; i < F.g.length; ++i)
		{
			var id = F.gi[i];
			groups[id] = 1;
			var t = F.g[i];
			UpdateFrameDataInternal(id, t[0], t[1], t[2], 1);
		}
		
		let UpdateThreadInfo = function(o)
		{
			let sn = SanitizeString(o.n);
			let TI = ThreadInfo[sn];
			if(!TI)
			{
				TI = { ids:o.gi, a:[], n:o.n, sn:sn };
				for(let i = 0; i < TI.ids.length; ++i)
				{
					TI.a.push(AllocClearedArray(FRAME_COUNT));
				}
				ThreadInfo[sn] = TI;
			}
			for(let i = 0; i < o.gi.length; ++i)
			{
				let id = o.gi[i];
				let t = o.g[i][1];
				let te = FrameData.Time[FrameData.Time.length-1];
				if(t > te)
				{
					console.log("fail!");
					// debugger;
				}
				let idx = TI.ids.indexOf(id);
				if(-1 == idx)
				{
					TI.ids.push(id);
					let a = AllocClearedArray(FRAME_COUNT);
					a[a.length-1] = o.g[i][1];
					TI.a.push(a);
				}
				else
				{
					let l = TI.a[idx].length;
					TI.a[idx][l-1] = o.g[i][1];
				}
			}
		};
		if(F.gt && F.gt.length)
		{
			for(let key in ThreadInfo)
			{
				let TI = ThreadInfo[key];
				for(let i = 0; i < TI.a.length; ++i)
				{
					PushIntoArray(TI.a[i], 0);
				}
			}


			for(let i = 0; i < F.gt.length; ++i)
			{
				let o = F.gt[i];
				UpdateThreadInfo(o);
			}
		}



		var ToDelete = new Array();
		for(var id in FrameData.TimerMap)
		{
			let FD = FrameData.TimerMap[id];
			if(!F.x[id] && !groups[id])
			{
				PushIntoArray(FD.Time,0.0);
				PushIntoArray(FD.TimeExcl, 0.0);
				PushIntoArray(FD.Count, 0);
				FindMaxTime(FD.Time);
				FD.EmptyFrames++;
			}
			else
			{
				FD.EmptyFrames = 0;
			}
			FD.FrameTime = FD.Time[FD.Time.length-1]; //Fix her..?
			FD.AggregateTime = FD.Time[FD.Time.length-1];

			if(FD.EmptyFrames == FD.Time.length)
			{
				ToDelete.push(id);
			}
		}
		for(var i = 0; i < ToDelete.length; ++i)
		{
			delete FrameData.TimerMap[ToDelete[i]];
		}
		FramePending++;


		if(TriggerAutoCapture)
		{
			Capture();
		}
		ReferenceGraphAutomaticGroup = 1.05 * GraphTimeGroupMax;
		ReferenceGraphAutomatic = 1.05 * GraphTimeMax;
		ReferenceHistoryAutomatic = 1.05 * HistoryTimeMax;
	}
	RequestDraw();
	ProfileLeave();
}

function WSMessage(event)
{
	// console.log(event.data);
	var Obj = JSON.parse(event.data);
	WSReceive++;
	WSReceiveBytes += event.data.length;
	var k = Obj.k;
	if(k == MSG_TIMER_TREE)
	{
		AddTimer(Obj.v);
	}
	else if(k == MSG_ENABLED)
	{
		EnableTimer(Obj.v);
	}
	else if(k == MSG_FRAME)
	{
		ProcessFrame(Obj.v);
	}
	else if(k == MSG_LOADSETTINGS)
	{
		OnLoadPreset(Obj.v, 1, Obj.ro);
	}
	else if(k == MSG_CURRENTSETTINGS)
	{
		OnLoadPreset(Obj.v, 0, Obj.ro);
	}
	else if(k == MSG_PRESETS)
	{
		AddPresets(Obj.v);
	}
	else if(k == MSG_COUNTERS)
	{
		ProcessCounters(Obj.v);
	}
	else if(k == MSG_FUNCTION_RESULTS)
	{
		FunctionQueryPending = null;
		if(FunctionQueryReceived< Obj.q)
		{
			FunctionQueryArray = Obj.v;
			FunctionQueryReceived = Obj.q;
			console.log('got result from query ' + Obj.q);
		}
		else
		{
			console.log('ignored result from query ' + Obj.q);
		}


	}
	else if(k == MSG_INACTIVE_FRAME)
	{
		ProcessFrame(Obj.v, 1);
	}
	else if(k == MSG_FUNCTION_NAMES)
	{
		for(var i = 0; i < Obj.v.length; ++i)
		{
			FunctionsInstrumented.push(Obj.v[i][0]);
			FunctionsInstrumentedModule.push(Obj.v[i][1]);
			FunctionsInstrumentedUnmangled.push(Obj.v[i][2]);

		}
	}
	else if(k == MSG_INSTRUMENT_ERROR)
	{
		var D = Obj.v.data;
		var F = Obj.v.functions;
		var ASM_SERVER = "http://microprofileasm.zapto.org"; 
		var url = ASM_SERVER + "/add/" + Obj.v.version + "/";
		console.log(JSON.stringify(D));
		console.log(JSON.stringify(F));
		for(var i = 0; i < D.length; ++i)
		{
			url = url + D[i].code + "/";
		}
		var s = "Failed to instrument the following functions:\n";

		for(var i = 0; i < F.length; ++i)
		{
			s = s + F[i] + "\n";
		}
		s = s + "\n\nPlease click \"OK\"to report the failed code segments\n"
		s = s + "this will open the following url in a hidden frame\n"
		s = s + url;
		s = s + "\n-No additional data will be sent\n-No symbol names will be sent";
		s = s + "\n[this popup can be auto-denied or auto-accepted from the settings menu]";
		if(Cookie.CodeReportMode != 2)
		{
			if(Cookie.CodeReportMode == 1 || confirm(s))
			{
				var iframe = document.createElement("iframe");
				iframe.onload = function(){document.body.removeChild(iframe); console.log("removed iframe");};
				console.log("opening url " + url);
				iframe.setAttribute("src", url);
				iframe.style.width = "100x";
				iframe.style.height = "100px";
				document.body.appendChild(iframe);
			}
		}
		console.log("got error "+ JSON.stringify(Obj.v));
	}
	// else if(k == MSG_MODULE_NAME)
	// {
	// 	console.log("MODULENAME..\n");
	// 	console.log(JSON.stringify(Obj.v));
	// 	ModuleState = Obj.v;

	// }
	else
	{
		console.log('hest!');
	}

}
function WSError(event)
{
	console.log('WSError');
}
function WSClose(event)
{
	console.log('WSClose');
	WSIsOpen = 0;
	window.document.title = "MicroProfile Live";
	FilterInputDiv.style['display'] = 'none';
}
function WSSendMessage(msgid)
{
	if(WSIsOpen)
	{
		var str = '' + msgid;
		WSSend++;
		WSSendBytes += str.length;
		WS.send(str);
	}
	else
	{
		if(msgid[0] == 'c')
		{
			console.log('failing to send ' + msgid);
		}
	}
}


function Connect()
{
	if(WS && (WS.readyState == 1 || WS.readyState == 0))
	{
		WSConnected = WS.readyState == 1;
		WSFail = 0;
		WSSeconds = 0;
	}
	else
	{
		WSConnected = 0;
		WSSeconds = (new Date() - WSOpenTime);
		if(!WS || WSSeconds > 2000)
		{
			if(WS)
			{
				WS.close();
				WS = null;
			}
			WSOpenTime = new Date();
			WSPath = "ws://" + WSHost + ":" + WSPort + "/ws";
			SetMessage('Connecting to ' + WSPath,5 * 1000, 1);
			WS = new WebSocket(WSPath);
			WS.onopen = WSOpen;
			WS.onmessage = WSMessage;
			WS.onerror = WSError;
			WS.onclose = WSClose;
			WSFail = 0;
		}
		else
		{
			WSFail++;
		}
	}
	RequestDraw();
}


function MouseDragPan()
{
	return MouseDragButton == 1 || MouseDragKeyShift;
}
function MouseDragSelectRange()
{
	return MouseDragState == MouseDragMove && (MouseDragButton == 3 || (MouseDragKeyShift&&MouseDragKeyCtrl));
}

function MouseHandleDrag()
{
	if(MouseDragTarget == CanvasDetailedView)
	{
		if(SubMenuActive == -1)
		{
			if(MouseDragSelectRange() && SubMenuActive == -1)
			{
				var xStart = MouseDragXStart;
				var xEnd = MouseDragX;
				if(xStart > xEnd)
				{
					var Temp = xStart;
					xStart = xEnd;
					xEnd = Temp;
				}
				if(xEnd - xStart > 1)
				{
					MouseDragActiveXStart = xStart;
					MouseDragActiveXEnd = xEnd;
				}
			}
			else if(MouseDragPan())
			{
				var X = MouseDragX - MouseDragXLast;
				var Y = MouseDragY - MouseDragYLast;
				if(X && MouseDragActiveXStart < MouseDragActiveXEnd)
				{
					MouseDragActiveXStart += X;
					MouseDragActiveXEnd += X;
				}
			}
			if(Settings.ViewActive == VIEW_BAR)
			{
				if(MouseDragKeyShift || MouseDragButton == 1)
				{
					var X = MouseDragX - MouseDragXLast;
					var Y = MouseDragY - MouseDragYLast;
					nOffsetBarsY -= Y;
					nOffsetBarsX -= X;
					if(nOffsetBarsY < 0)
					{
						nOffsetBarsY = 0;
					}
					if(nOffsetBarsX < 0)
					{
						nOffsetBarsX = 0;
					}
				}
			}
			if(Settings.ViewActive == VIEW_COUNTERS)
			{
				if(MouseDragKeyShift || MouseDragButton == 1)
				{
					var Y = MouseDragY - MouseDragYLast;
					nOffsetCountersY -= Y;
					if(nOffsetCountersY < 0)
					{
						nOffsetCountersY = 0;
					}
				}
			}
		}
		else if(SubMenuActive == SubMenuTimers || SubMenuActive == SubMenuGroup || SubMenuActive == SubMenuFunctions)
		{
			if(MouseDragKeyShift || MouseDragButton == 1)
			{
				var Y = MouseDragY - MouseDragYLast;
				if(SubMenuActive == SubMenuTimers)
				{
					nOffsetMenuTimers -= Y;
					if(nOffsetMenuTimers < 0)
					{
						nOffsetMenuTimers = 0;
					}
				}
				else if(SubMenuActive == SubMenuFunctions)
				{
					nOffsetMenuFunctions -= Y;
					if(nOffsetMenuFunctions < 0)
					{
						nOffsetMenuFunctions = 0;
					}
				}
				else
				{
					nOffsetMenuGroup -= Y;
					if(nOffsetMenuGroup < 0)
					{
						nOffsetMenuGroup = 0;
					}
				}
			}
		}
	}
	MouseDragXLast = MouseDragX;
	MouseDragYLast = MouseDragY;	
}
function MouseHandleDragEnd()
{
	if(MouseDragTarget == CanvasDetailedView)
	{

	}
}

function MouseHandleDragClick()
{
	if(SubMenuActive == -1)
	{	
		if(nHoverCounter != -1)
		{
			if(TimerArray[nHoverCounter].firstchild != -1)
			{
				TimerArray[nHoverCounter].closed = !TimerArray[nHoverCounter].closed;
			}
			else
			{
				TimerArray[nHoverCounter].Expanded = !TimerArray[nHoverCounter].Expanded;
			}
			Draw(1);
		}
	}
	if(MouseInCaptureButton)
	{
		TriggerCapture();
	}
}

function MapMouseButton(event)
{
	if(event.button == 1 || event.which == 1)
	{
		return 1;
	}
	else if(event.button == 3 || event.which == 3)
	{
		return 3;
	}
	else
	{
		return 0;
	}
}

function MouseDragReset()
{
	MouseDragState = MouseDragOff;
	MouseDragTarget = 0;
	MouseDragKeyShift = 0;
	MouseDragKeyCtrl = 0;
	MouseDragButton = 0;
}
function MouseDragKeyUp()
{
	if((MouseDragKeyShift && !KeyShiftDown) || (MouseDragKeyCtrl && !KeyCtrlDown))
	{
		MouseHandleDragEnd();
		MouseDragReset();
	}
}
function MouseDrag(Source, Event)
{
	if(Source == MouseDragOff || (MouseDragTarget && MouseDragTarget != Event.target))
	{
		MouseDragReset();
		return;
	}

	var LocalRect = Event.target.getBoundingClientRect();
	MouseDragX = Event.clientX - LocalRect.left;
	MouseDragY = Event.clientY - LocalRect.top;
	if(MouseDragState == MouseDragMove)
	{
		var dx = Math.abs(MouseDragX - MouseDragXStart);
		var dy = Math.abs(MouseDragY - MouseDragYStart);
		if((Source == MouseDragUp && MapMouseButton(Event) == MouseDragButton) ||
			(MouseDragKeyCtrl && !KeyCtrlDown) ||
			(MouseDragKeyShift && !KeyShiftDown))
		{
			MouseHandleDragEnd();
			MouseDragReset();
			return;
		}
		else
		{
			MouseHandleDrag();
		}
	}
	else if(MouseDragState == MouseDragOff)
	{
		if(Source == MouseDragDown || KeyShiftDown || KeyCtrlDown)
		{
			MouseDragTarget = Event.target;
			MouseDragButton = MapMouseButton(Event);
			MouseDragState = MouseDragDown;
			MouseDragXStart = MouseDragX;
			MouseDragYStart = MouseDragY;
			MouseDragKeyCtrl = 0;
			MouseDragKeyShift = 0;

			if(KeyShiftDown || KeyCtrlDown)
			{
				MouseDragKeyShift = KeyShiftDown;
				MouseDragKeyCtrl = KeyCtrlDown;
				MouseDragState = MouseDragMove;
			}
		}
	}
	else if(MouseDragState == MouseDragDown)
	{
		if(Source == MouseDragUp)
		{
			MouseHandleDragClick();
			MouseDragReset();
		}
		else if(Source == MouseDragMove)
		{
			var dx = Math.abs(MouseDragX - MouseDragXStart);
			var dy = Math.abs(MouseDragY - MouseDragYStart);
			if(dx+dy>1)
			{
				MouseDragState = MouseDragMove;
			}
		}
	}
	MouseDragXLast = MouseDragX;
	MouseDragYLast = MouseDragY;
}


function MouseMove(evt)
{
    evt.preventDefault();
	var rect = evt.target.getBoundingClientRect();
	var x = evt.clientX - rect.left;
	var y = evt.clientY - rect.top;
	MouseX = x;
	MouseY = y;
	MouseMoveTime = new Date();
    MouseDrag(MouseDragMove, evt);
    RequestDraw();
}


function MouseSortClick()
{
	if(SubMenuActive == -1)
	{
		if(SortColumnMouseOverNext)
		{
			if(SortColumnMouseOverNext == Settings.SortColumnName)
			{
				Settings.SortColumnOrderFlip =  1 - Settings.SortColumnOrderFlip;
			}
			else
			{
				Settings.SortColumnOrderFlip = 0;
			}

			Settings.SortColumnName = SortColumnMouseOverNext;
			SortColumnMouseOverNext = null;
		}
	}
}


function MouseButton(bPressed, evt)
{
    evt.preventDefault();
	MouseReleased = !bPressed;
	MouseDrag(bPressed ? MouseDragDown : MouseDragUp, evt);
	if(!bPressed)
		MouseSortClick();
    RequestDraw();
}

function MouseOut(evt)
{
	MouseDrag(MouseDragOff, evt);
	KeyCtrlDown = 0;
	KeyShiftDown = 0;
	// MouseDragButton = 0;
	// nHoverToken = -1;
	// RangeCpu = RangeInit();
}

function MouseWheel(e)
{
//     var e = window.event || e;
//     var delta = (e.wheelDelta || e.detail * (-120));
//     ZoomGraph((-4 * delta / 120.0) | 0);
//     Draw(1);
}

function KeyUp(evt)
{
	var k = evt.keyCode;
	var InputActive = SubMenuActive == SubMenuTimers || SubMenuActive == SubMenuGroup || SubMenuActive == SubMenuFunctions || SubMenuActive == SubMenuPatched || SubMenuActive == SubMenuModules;
	if(!InputActive)
	{
		if(k == 220)
		{
			ProfileMode = !ProfileMode;
		}
		if(k == 191)
		{
			WSPort++;
			if(WSPort > 1338+2)
			{
				WSPort = 1338;
			}
			if(WS)
			{
				WS.close();
				WS = null;
			}
		}
		if(k == 32)
		{
			if(0) //for debugging.
			{
				console.log("FrameData =");
				console.log(JSON.stringify(FrameData));
				console.log("TimerArray =");
				console.log(JSON.stringify(TimerArray));
			}
			WSSendMessage("f");
		}
		if(k == 88)
		{
			ToggleView();
		}
		if(k == 13)
		{
			TriggerCapture();
		}
		if(k == 72)
		{
			ShowHelp(0, 1);
		}


	}
	if(k == 27)
	{
		if(FilterInput.value.trim() != "")
		{
			FilterInput.value = "";
		}
		else
		{
			EnableMenu(-1);
		}

		MouseDragActiveXStart = MouseDragActiveXEnd = -1;
		Settings.SortColumnName = "";
		ShowHelp(0);
	}
	if(k == 192)
	{
		Settings.ViewCompressed = Settings.ViewCompressed ? 0 : 1;
		ResizeCanvas();
	}

	if(evt.keyCode == 17)
	{
		KeyCtrlDown = 0;
		MouseDragKeyUp();
	}
	if(evt.keyCode == 16)
	{
		KeyShiftDown = 0;
		MouseDragKeyUp();
	}
}

function KeyDown(evt)
{
	// console.log("keydow ", k);
	if(evt.keyCode == 17)
	{
		KeyCtrlDown = 1;
		MouseDragKeyUp();
	}
	if(evt.keyCode == 16)
	{
		KeyShiftDown = 1;
		MouseDragKeyUp();
	}
}


function SetupEvents()
{
	var mousewheelevt = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel"; //FF doesn't recognize mousewheel as of 
	CanvasDetailedView.addEventListener('mousemove', MouseMove, false);
	CanvasDetailedView.addEventListener('mousedown', function(evt) { MouseButton(true, evt); });
	CanvasDetailedView.addEventListener('mouseup', function(evt) { MouseButton(false, evt); } );
	CanvasDetailedView.addEventListener('mouseout', MouseOut);
	CanvasDetailedView.addEventListener("contextmenu", function (e) { e.preventDefault(); }, false);
	CanvasDetailedView.addEventListener(mousewheelevt, MouseWheel, false);
	window.addEventListener('keydown', KeyDown);
	window.addEventListener('keyup', KeyUp);
	window.addEventListener('resize', ResizeCanvas, false);
}

function DrawToolTip(StringArray, Canvas, x, y)
{
	if(!ShowMenu())
	{
		return;
	}
	let colors;
	let a = StringArray;
	if(StringArray.c)
	{
		a = StringArray.a;
		colors = StringArray.c;
	}
	var context = Canvas.getContext('2d');
	context.font = Font;
	var WidthArray = Array(a.length);
	var nMaxWidth = 0;
	var nHeight = 0;
	for(var i = 0; i < a.length; i += 2)
	{
		var nWidth0 = context.measureText(a[i]).width;
		var nWidth1 = context.measureText(a[i+1]).width;
		var nSum = nWidth0 + nWidth1;
		WidthArray[i] = nWidth0;
		WidthArray[i+1] = nWidth1;
		if(nSum > nMaxWidth)
		{
			nMaxWidth = nSum;
		}
		nHeight += BoxHeight;
	}
	nMaxWidth += 15;
	//bounds check.
	x = Math.max(0, x - 10 - nMaxWidth);
	var CanvasRect = Canvas.getBoundingClientRect();
	if(y + nHeight > CanvasRect.height)
	{
		y = CanvasRect.height - nHeight;
		x += 20;
	}
	if(x + nMaxWidth > CanvasRect.width)
	{
		x = CanvasRect.width - nMaxWidth;
	}

	context.fillStyle = 'black';
	context.fillRect(x-1, y, nMaxWidth+2, nHeight);
	context.fillStyle = 'white';

	var XPos = x;
	var XPosRight = x + nMaxWidth;
	var YPos = y + BoxHeight-2;
	context.fillStyle = 'white';
	for(i = 0; i < a.length; i += 2)
	{
		if(colors)
			context.fillStyle = colors[i];
		context.fillText(a[i], XPos, YPos);
		if(colors)
			context.fillStyle = colors[i+1];
		context.fillText(a[i+1], XPosRight - WidthArray[i+1], YPos);
		YPos += BoxHeight;
	}
	context.fillStyle = 'white';
}

function DrawPlotf(Canvas)
{
	return;
	var context = Canvas.getContext('2d');
	context.font = Font;
	var WidthArray = Array(PlotfArray.length);
	var nMaxWidth = 0;
	var nHeight = 0;

	context.font = Font;
	for(i = 0; i < PlotfArray.length; i++)
	{
		var nWidth = context.measureText(PlotfArray[i]).width;
		WidthArray[i] = nWidth;
		if(nWidth > nMaxWidth)
		{
			nMaxWidth = nWidth;
		}
		nHeight += BoxHeight;
	}
	nMaxWidth += 15;
	var x = 0;
	var y = 0;

	context.fillStyle = 'black';
	context.fillRect(x-1, y, nMaxWidth+2, nHeight);
	context.fillStyle = 'white';

	var XPos = x;
	var XPosRight = x + nMaxWidth;
	var YPos = y + BoxHeight-2;
	for(i = 0; i < PlotfArray.length; i++)
	{
		context.fillText(PlotfArray[i], XPos, YPos);
		YPos += BoxHeight;
	}
}


function ShiftRight10(v)
{
	if(v > 1024)
	{
		return v / 1024.0;
	}
	else
	{
		return v >> 10;
	}
}

function FormatCounter(Format, Counter)
{
	if(!Counter)
	{
		return '0';
	}
	var Negative = 0;
	if(Counter < 0)
	{
		Counter = -Counter;
		Negative = 1;
		if(Counter < 0) // handle INT_MIN
		{
			Counter = -(Counter+1);
			if(Counter < 0)
			{
				return '?';
			}
		}
	}
	var str = Negative ? '-' :'' ;
	if(Format == FormatCounterDefault)
	{
		var Seperate = 0;
		var result = '';
		while (Counter)
		{
			if (Seperate)
			{
				result += '.';
			}
			Seperate = 1;
			for (var i = 0; Counter && i < 3; ++i)
			{
				var Digit = Math.floor(Counter % 10);
				Counter = Math.floor(Counter / 10);
				result += '' + Digit;
			}
		}

		for(var i = 0; i < result.length; ++i)
		{
			str += result[result.length-1-i];
		}
		return str;
	}
	else if(Format == FormatCounterBytes)
	{
		var Shift = 0;
		var Divisor = 1;
		var CountShifted = ShiftRight10(Counter);
		while(CountShifted)
		{
			Divisor <<= 10;
			CountShifted = ShiftRight10(CountShifted);
			Shift++;
		}
		if(Shift)
		{
			return str + (Counter / Divisor).toFixed(2) + '' + FormatCounterBytesExt[Shift];
		}
		else
		{
			return str + Counter.toFixed(2) + '' + FormatCounterBytesExt[0];
		}
	}
	return '?';
}
function DrawCounterView(View, LocalMouseX, LocalMouseY, SubIndex)
{
	var TimerMap = FrameData.TimerMap;
	if(!TimerMap)
		return;


	ProfileEnter("DrawCounterView");

	var Canvas = View.Canvas[View.BackBuffer];
	var context = Canvas.getContext('2d');
	context.clearRect(0, 0, View.w, View.h);



	var Height = BoxHeight;
	var Width = nWidth;
	var nTotalRows = CounterArray.length;
	var nTotalRowPixels = nTotalRows * Height;
	var nFrameRows = nHeight - BoxHeight;
	if(nOffsetCountersY + nFrameRows > nTotalRowPixels && nTotalRowPixels > nFrameRows)
	{
		nOffsetCountersY = nTotalRowPixels - nFrameRows;
	}
	var CounterWidth = 150;
	var Y = -nOffsetCountersY + BoxHeight;
	var X = 0;
	var nColorIndex = 0;
	context.fillStyle = 'white';
	context.font = Font;
	var bMouseIn = 0;
	function DrawHeaderSplitSingle(Header, Width)
	{
		context.fillStyle = 'white';
		context.fillText(Header, X, Height-FontAscent);
		X += Width;
		context.fillStyle = nBackColorOffset;
		context.fillRect(X-3, 0, 1, nHeight);
	}
	function DrawHeaderSplitSingleRight(Header, Width)
	{
		X += Width;
		context.fillStyle = 'white';
		context.textAlign  = 'right';
		context.fillText(Header, X - FontWidth, Height-FontAscent);
		context.fillStyle = nBackColorOffset;
		context.fillRect(X, 0, 1, nHeight);
		context.textAlign  = 'left';
	}
	var TimerLen = 6;
	var TimerWidth = TimerLen * FontWidth;
	nHoverCounter = -1;
	function CounterIndent(Level)
	{
		return Level * 4 * FontWidth;
	}
	function Max(a, b)
	{
		return a > b ? a : b;
	}

	function DrawCounterRecursive(Index)
	{
		var Counter = TimerArray[Index];
		if(Counter.idtype == TYPE_COUNTER)
		{
			var Indent = CounterIndent(Counter.depth-1);
			var X = 0;
			nColorIndex = 1-nColorIndex;
			var HeightExpanded = Counter.Expanded ? Height * 5 : Height;

			bMouseIn = LocalMouseY >= Y && LocalMouseY < Y + HeightExpanded;
			if(bMouseIn)
			{
				nHoverCounter = Index;
			}
			var bgcolor = bMouseIn ? nBackColorOffset : nBackColors[nColorIndex];
			context.fillStyle = bgcolor;
			context.fillRect(0, Y, Width, HeightExpanded);
			context.fillStyle = 'white';
			var c = Counter.closed ? '*' : ' ';
			context.fillText(c + Counter.name, Indent, Y+Height-FontAscent);
			X += CounterNameWidth;
			X += CounterValueWidth - FontWidth;
			context.textAlign = 'right';
			context.fillText(Counter.formatted, X, Y+Height-FontAscent);
			context.textAlign = 'left';
			X += FontWidth * 4;
			var Y0 = Y + 1;
			if(Counter.limit != 0)
			{
				context.fillText(Counter.formattedlimit, X, Y+Height-FontAscent);
				X += CounterLimitWidth;
				var X0 = X + 1;
				context.fillStyle = 'white';
				context.fillRect(X0, Y0, Counter.boxprc * (CounterWidth-2), Height-2);
				context.fillStyle = bgcolor;
				context.fillRect(X0+1, Y0+1, Counter.boxprc * (CounterWidth-4), Height-4);
				context.fillStyle = 'cyan';
				context.fillRect(X0+1, Y0+1, Counter.counterprc * (CounterWidth-4), Height-4);
				X += CounterWidth + 10;
			}
			else
			{
				X += CounterLimitWidth;
				X += CounterWidth + 10;
			}
			
			if(Counter.minvalue != Counter.maxvalue)
			{
				var CounterHistory = Counter.counterhistory;
				var Prc = CounterHistory.prc;
				context.fillStyle = 'cyan';
				context.strokeStyle = 'cyan';
				context.globalAlpha = 0.5;
				context.beginPath();
				var x = X;
				var YBase = Y0 + HeightExpanded-1;
				var YOffset = -(HeightExpanded-2);

				context.moveTo(X, Y0);
				for(var i = 0; i < Prc.length; ++i)
				{
					context.moveTo(x, YBase);
					context.lineTo(x, YBase + Prc[i] * YOffset);
					x += 1;
				}
				context.stroke();

				x = X;
				context.globalAlpha = 1.0;
				context.beginPath();
				context.moveTo(X, YBase);

				for(var i = 0; i < Prc.length; ++i)
				{
					context.lineTo(x, YBase + Prc[i] * YOffset);
					x += 1;
				}
				context.stroke();
				if(bMouseIn)
				{
					var MouseGraphX = Math.floor(LocalMouseX - X);
					if(MouseGraphX >= 0 && MouseGraphX < CounterHistory.history.length)
					{
						context.fillStyle = 'white';
						var Formatted = FormatCounter(Counter.format, CounterHistory.history[MouseGraphX]);
						context.fillText(Formatted, X, Y+Height-FontAscent);
					}
					context.strokeStyle = 'orange';
					context.beginPath();
					var CrossX = X + MouseGraphX;
					var CrossY = YBase + Prc[MouseGraphX] * YOffset;
					context.moveTo(CrossX-2, CrossY-2);
					context.lineTo(CrossX+2, CrossY+2);
					context.moveTo(CrossX+2, CrossY-2);
					context.lineTo(CrossX-2, CrossY+2);
					context.stroke();

				}
				X += Prc.length + 5;
				context.fillStyle = 'white';
				context.fillText( FormatCounter(Counter.format, Counter.minvalue), X, Y + Height - FontAscent);
				X += CounterWidth + 5;
				context.fillText( FormatCounter(Counter.format, Counter.maxvalue), X, Y + Height - FontAscent);
				X += CounterWidth + 5;
			}
			Y += HeightExpanded;
		}

		if(Index == 0 || (!Counter.closed && Counter.idtype == TYPE_COUNTER))
		{
			var ChildIndex = Counter.firstchild;
			while(ChildIndex != -1)
			{
				DrawCounterRecursive(ChildIndex);
				ChildIndex = TimerArray[ChildIndex].sibling;
			}
		}
	}

	DrawCounterRecursive(0);

	X = 0;
	context.fillStyle = nBackColorOffset;
	context.fillRect(0, 0, Width, Height);
	context.fillStyle = 'white';
	DrawHeaderSplitSingle('Name', CounterNameWidth);
	DrawHeaderSplitSingleRight('Value', CounterValueWidth + (FontWidth+1));
	DrawHeaderSplitSingle('Limit', CounterLimitWidth + CounterWidth + 3 * (FontWidth+1));
	ProfileLeave();
}

function ShowHelp(Show, Toggle)
{
	var HelpWindow = document.getElementById('helpwindow');
	if(Toggle)
	{
		if(HelpWindow.style['display'] == 'block')
		{
			HelpWindow.style['display'] = 'none';
		}
		else
		{
			HelpWindow.style['display'] = 'block';			
		}
	}
	else
	{
		if(Show)
		{
			HelpWindow.style['display'] = 'block';
		}
		else
		{
			HelpWindow.style['display'] = 'none';
		}
	}
}

function ParseUrl()
{
	var path = window.location.pathname;
	var idx = path.indexOf('/');
	if(idx < 0)
		return;
	var StrCommand = path.substring(idx+1);
	idx = StrCommand.indexOf('/');
	if(idx < 0)
		return;
	var StrSettings = StrCommand.substring(idx+1);
	PresetToLoad = StrSettings;
	PresetToLoadRO = StrCommand[0] == 'b';
}


function GetCookie()
{
	var result = document.cookie.match(/fisk=([^;]+)/);
	if(result && result.length > 0)
	{
		var Obj = JSON.parse(result[1]);
		if(!Obj.offline)
		{
			var C = {};
			C.offline = Obj;
			Obj = C;
		}
		return Obj;
	}
	else
	{
		return {offline:{},live:{}};
	}
}

function ReadCookie()
{
	var C = GetCookie().live;
	for(var i in C)
	{
		Cookie[i] = C[i];
	}
}
function WriteCookie()
{
	var C = GetCookie();
	C.live = {};
	for(var i in Cookie)
	{
		C.live[i] = Cookie[i];
	}

	var date = new Date();
	date.setFullYear(2099);
	var cookie = 'fisk=' + JSON.stringify(C) + ';expires=' + date;
	document.cookie = cookie;
}

ReadCookie();
ParseUrl();
ResizeCanvas();
SetupEvents();
InitMenu();
setInterval(Connect, 100);
RequestDraw();


</script>
</body>
</html>      
